init
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
154
.gitignore
vendored
Normal file
154
.gitignore
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
tests/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 yoggys
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
76
README.md
Normal file
76
README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 7TV EventAPI Wrapper
|
||||
|
||||
[](https://dc.yoggies.dev/)
|
||||
[](https://www.python.org/downloads/release/python-380/)
|
||||
[](https://pypi.org/project/eventapi/)
|
||||
[](https://pypi.org/project/eventapi/)
|
||||
|
||||
This is a Python wrapper for the 7TV EventAPI, which provides async access to the websocket events on
|
||||
their [Emote Platform](https://7tv.app).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8 or higher
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the 7TV EventAPI Wrapper using pip. Open your terminal and run the following command:
|
||||
|
||||
```shell
|
||||
pip install eventapi
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Here's an simple example of how to use wrapper:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from eventapi.EventApi import EventApi
|
||||
from eventapi.WebSocket import (
|
||||
Dispatch,
|
||||
EventType,
|
||||
ResponseTypes,
|
||||
SubscriptionCondition,
|
||||
SubscriptionData,
|
||||
)
|
||||
|
||||
|
||||
async def callback(data: ResponseTypes):
|
||||
if isinstance(data, Dispatch):
|
||||
print(f"Event data type: {data.type}")
|
||||
print(f"Event data body: {data.body}")
|
||||
else:
|
||||
print(f"Raw event data: {data.raw_data}")
|
||||
|
||||
|
||||
async def main():
|
||||
# create instance of EventApi with message callback
|
||||
app = EventApi(callback=callback)
|
||||
|
||||
# connect to websocket
|
||||
await app.connect()
|
||||
|
||||
# create subscription with specified condition
|
||||
condition = SubscriptionCondition(object_id="6433b7cec07d26f890dd2d01")
|
||||
subscription = SubscriptionData(
|
||||
subscription_type=EventType.EMOTE_SET_ALL, condition=condition
|
||||
)
|
||||
await app.subscribe(subscription_data=subscription)
|
||||
|
||||
# run forever
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! If you encounter any issues or have suggestions for improvements, please open an issue or
|
||||
submit a pull request on the [GitHub repository](https://github.com/yoggys/unbelievaboat).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
||||
129
eventapi/EventApi.py
Normal file
129
eventapi/EventApi.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import asyncio
|
||||
import json
|
||||
import socket
|
||||
from typing import Any, Callable, Coroutine, Optional
|
||||
|
||||
import websockets
|
||||
|
||||
from eventapi.Exceptions import (
|
||||
ConnectionException,
|
||||
NoActiveConnectionException,
|
||||
NoActiveSubscriptionsException,
|
||||
SubscriptionException,
|
||||
)
|
||||
from eventapi.WebSocket import (
|
||||
Ack,
|
||||
Dispatch,
|
||||
EndOfStream,
|
||||
Error,
|
||||
Heartbeat,
|
||||
Hello,
|
||||
Reconnect,
|
||||
ResponseTypes,
|
||||
SubscriptionData,
|
||||
WebsocketMessageType,
|
||||
)
|
||||
|
||||
|
||||
class EventApi:
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[ResponseTypes], Coroutine] = None,
|
||||
websocket_url: Optional[str] = None,
|
||||
) -> None:
|
||||
self.WS_URL: str = websocket_url or "wss://events.7tv.io/v3"
|
||||
self.ws: Optional[websockets.WebSocketClientProtocol] = None
|
||||
self.handler: Optional[asyncio.Task] = None
|
||||
|
||||
self.previous_session_id: Optional[str] = None
|
||||
self.session_id: Optional[str] = None
|
||||
self.subscription_limit: int = 100
|
||||
self.subscriptions: list[SubscriptionData] = []
|
||||
self.callback = callback
|
||||
|
||||
async def connect(self) -> None:
|
||||
try:
|
||||
self.ws = await websockets.connect(self.WS_URL)
|
||||
self.handler = asyncio.create_task(self.message_handler())
|
||||
except (websockets.exceptions.WebSocketException, socket.gaierror):
|
||||
raise ConnectionException()
|
||||
|
||||
async def reconnect(self) -> None:
|
||||
self.handler.cancel()
|
||||
self.handler = None
|
||||
if not self.ws.closed:
|
||||
await self.ws.close()
|
||||
self.ws = None
|
||||
await self.connect()
|
||||
|
||||
async def message_handler(self) -> None:
|
||||
try:
|
||||
while True:
|
||||
message = await self.ws.recv()
|
||||
if isinstance(message, bytes):
|
||||
message = message.decode("utf-8")
|
||||
asyncio.create_task(self.on_message(message))
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
asyncio.create_task(self.reconnect())
|
||||
|
||||
async def on_message(self, message: str) -> None:
|
||||
message = json.loads(message)
|
||||
parsed_message = await self.parse_message(message)
|
||||
if self.callback and parsed_message:
|
||||
asyncio.create_task(self.callback(parsed_message))
|
||||
|
||||
async def parse_message(self, message: dict[str, Any]) -> Optional[ResponseTypes]:
|
||||
message_data = message.get("d")
|
||||
message_code = message.get("op")
|
||||
|
||||
if message_code == WebsocketMessageType.DISPATCH:
|
||||
return Dispatch(message_data)
|
||||
elif message_code == WebsocketMessageType.HELLO:
|
||||
parsed_message = Hello(message_data)
|
||||
if self.session_id:
|
||||
asyncio.create_task(
|
||||
self.ws.send(
|
||||
json.dumps({"op": 34, "d": {"session_id": self.session_id}})
|
||||
)
|
||||
)
|
||||
self.session_id = parsed_message.session_id
|
||||
self.subscription_limit = parsed_message.subscription_limit
|
||||
return parsed_message
|
||||
elif message_code == WebsocketMessageType.HEARTBEAT:
|
||||
return Heartbeat(message_data)
|
||||
elif message_code == WebsocketMessageType.RECONNECT:
|
||||
asyncio.create_task(self.reconnect())
|
||||
return Reconnect(message_data)
|
||||
elif message_code == WebsocketMessageType.ACK:
|
||||
parsed_message = Ack(message_data)
|
||||
if parsed_message.command == "RESUME" and not parsed_message.data.get(
|
||||
"success"
|
||||
):
|
||||
for s in self.subscriptions:
|
||||
asyncio.create_task(self.subscribe(s))
|
||||
return parsed_message
|
||||
elif message_code == WebsocketMessageType.ERROR:
|
||||
return Error(message_data)
|
||||
elif message_code == WebsocketMessageType.END_OF_STREAM:
|
||||
parsed_message = EndOfStream(message_data)
|
||||
if parsed_message.should_reconnect:
|
||||
asyncio.create_task(self.reconnect())
|
||||
return parsed_message
|
||||
|
||||
async def subscribe(self, subscription_data: SubscriptionData) -> None:
|
||||
if not self.ws or self.ws.closed:
|
||||
raise NoActiveConnectionException()
|
||||
if len(self.subscriptions) + 1 > self.subscription_limit:
|
||||
raise SubscriptionException(limit=self.subscription_limit)
|
||||
if subscription_data not in self.subscriptions:
|
||||
self.subscriptions.append(subscription_data)
|
||||
await self.ws.send(json.dumps({"op": 35, "d": subscription_data.data}))
|
||||
|
||||
async def unsubscribe(self, subscription_data: SubscriptionData) -> None:
|
||||
if not self.ws or self.ws.closed:
|
||||
raise NoActiveConnectionException()
|
||||
if len(self.subscriptions) == 0:
|
||||
raise NoActiveSubscriptionsException()
|
||||
if subscription_data in self.subscriptions:
|
||||
self.subscriptions.remove(subscription_data)
|
||||
await self.ws.send(json.dumps({"op": 36, "d": subscription_data.data}))
|
||||
39
eventapi/Exceptions.py
Normal file
39
eventapi/Exceptions.py
Normal file
@@ -0,0 +1,39 @@
|
||||
class EventApiException(Exception):
|
||||
"""
|
||||
Base class for all exceptions defined by EventApi.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class SubscriptionException(EventApiException):
|
||||
"""
|
||||
Thrown when a subscription limit is exceeded.
|
||||
|
||||
Attributes:
|
||||
limit (int): The maximum number of subscriptions.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, limit: int) -> None:
|
||||
self.limit = limit
|
||||
|
||||
|
||||
class NoActiveSubscriptionsException(EventApiException):
|
||||
"""
|
||||
Thrown when there are no active subscriptions.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ConnectionException(EventApiException):
|
||||
"""
|
||||
Thrown when a websocket connection fails.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class NoActiveConnectionException(EventApiException):
|
||||
"""
|
||||
Thrown when a trying to send a data when there is no active WebSocket connection.
|
||||
|
||||
"""
|
||||
210
eventapi/WebSocket.py
Normal file
210
eventapi/WebSocket.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import json
|
||||
from typing import Any, Optional, Self, Union
|
||||
|
||||
|
||||
class DotDict(dict):
|
||||
__getattr__ = dict.get
|
||||
|
||||
|
||||
class WebsocketMessageType(DotDict):
|
||||
DISPATCH = 0
|
||||
HELLO = 1
|
||||
HEARTBEAT = 2
|
||||
RECONNECT = 4
|
||||
ACK = 5
|
||||
ERROR = 6
|
||||
END_OF_STREAM = 7
|
||||
IDENTIFY = 33
|
||||
RESUME = 34
|
||||
SUBSCRIBE = 35
|
||||
UNSUBSCRIBE = 36
|
||||
SIGNAL = 37
|
||||
|
||||
|
||||
class ServerCloseCodes(DotDict):
|
||||
SERVER_ERROR = 4000
|
||||
UNKNOWN_OPERATION = 4001
|
||||
INVALID_PAYLOAD = 4002
|
||||
AUTH_FAILURE = 4003
|
||||
ALREADY_IDENTIFIED = 4004
|
||||
RATE_LIMITED = 4005
|
||||
RESTART = 4006
|
||||
MAINTENANCE = 4007
|
||||
TIMEOUT = 4008
|
||||
ALREADY_SUBSCRIBED = 4009
|
||||
NOT_SUBSCRIBED = 4010
|
||||
INSUFFICIENT_PRIVILEGE = 4011
|
||||
|
||||
RECONNECT_CODES = [4000, 4006, 4007, 4008]
|
||||
|
||||
|
||||
class EventType(DotDict):
|
||||
SYSTEM_ANNOUNCEMENT = "system.announcement"
|
||||
SYSTEM_ALL = "system.*"
|
||||
EMOTE_CREATE = "emote.create"
|
||||
EMOTE_UPDATE = "emote.update"
|
||||
EMOTE_DELETE = "emote.delete"
|
||||
EMOTE_ALL = "emote.*"
|
||||
EMOTE_SET_CREATE = "emote_set.create"
|
||||
EMOTE_SET_UPDATE = "emote_set.update"
|
||||
EMOTE_SET_DELETE = "emote_set.delete"
|
||||
EMOTE_SET_ALL = "emote_set.*"
|
||||
USER_CREATE = "user.create"
|
||||
USER_UPDATE = "user.update"
|
||||
USER_DELETE = "user.delete"
|
||||
USER_ADD_CONNECTION = "user.add_connection"
|
||||
USER_UPDATE_CONNECTION = "user.update_connection"
|
||||
USER_DELETE_CONNECTION = "user.delete_connection"
|
||||
USER_ALL = "user.*"
|
||||
COSMETIC_CREATE = "cosmetic.create"
|
||||
COSMETIC_UPDATE = "cosmetic.update"
|
||||
COSMETIC_DELETE = "cosmetic.delete"
|
||||
COSMETIC_ALL = "cosmetic.*"
|
||||
ENTITLEMENT_CREATE = "entitlement.create"
|
||||
ENTITLEMENT_UPDATE = "entitlement.update"
|
||||
ENTITLEMENT_DELETE = "entitlement.delete"
|
||||
ENTITLEMENT_ALL = "entitlement.*"
|
||||
|
||||
|
||||
class UserConnection:
|
||||
def __init__(self, data: dict[str, Any]):
|
||||
self.id: str = data.get("id")
|
||||
self.username: str = data.get("username")
|
||||
self.display_name: str = data.get("display_name")
|
||||
self.platform: str = data.get("platform")
|
||||
self.linked_at: int = data.get("linked_at")
|
||||
self.emote_capacity: int = data.get("emote_capacity")
|
||||
self.emote_set_id: str = data.get("emote_set_id")
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self, data: dict[str, Any]):
|
||||
self.id: str = data.get("id")
|
||||
self.username: str = data.get("username")
|
||||
self.display_name: str = data.get("display_name")
|
||||
self.avatar_url: str = data.get("avatar_url")
|
||||
self.style: Optional[dict[str, Any]] = data.get("style")
|
||||
self.roles: Optional[list[str]] = data.get("roles")
|
||||
self.connections: Optional[list[UserConnection]] = data.get("connections")
|
||||
|
||||
|
||||
class ChangeField:
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.key: str = data.get("key")
|
||||
self.index: int = data.get("index")
|
||||
self.nested: bool = data.get("nested")
|
||||
self.old_value: Optional[dict[str, Any]] = data.get("nested")
|
||||
|
||||
value: Optional[Union[list[Self], dict[str, Any]]] = data.get("value")
|
||||
self.value: Optional[Union[list[Self], dict[str, Any]]] = (
|
||||
value
|
||||
if not value or isinstance(value, dict)
|
||||
else [ChangeField(c) for c in value]
|
||||
)
|
||||
|
||||
|
||||
class ChangeMap:
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.id: str = data.get("id")
|
||||
self.kind: int = data.get("kind")
|
||||
self.contextual: Optional[bool] = data.get("contextual")
|
||||
self.actor: User = User(data.get("actor"))
|
||||
self.added: Optional[list[ChangeField]] = [
|
||||
ChangeField(d) for d in data.get("added", [])
|
||||
]
|
||||
self.updated: Optional[list[ChangeField]] = [
|
||||
ChangeField(d) for d in data.get("updated", [])
|
||||
]
|
||||
self.removed: Optional[list[ChangeField]] = [
|
||||
ChangeField(d) for d in data.get("removed", [])
|
||||
]
|
||||
self.pushed: Optional[list[ChangeField]] = [
|
||||
ChangeField(d) for d in data.get("pushed", [])
|
||||
]
|
||||
self.pulled: Optional[list[ChangeField]] = [
|
||||
ChangeField(d) for d in data.get("pulled", [])
|
||||
]
|
||||
|
||||
|
||||
class SubscriptionCondition:
|
||||
def __init__(
|
||||
self,
|
||||
object_id: Optional[str] = None,
|
||||
connection_id: Optional[str] = None,
|
||||
host_id: Optional[str] = None,
|
||||
) -> None:
|
||||
self.data = {}
|
||||
if object_id:
|
||||
self.data["object_id"] = object_id
|
||||
if connection_id:
|
||||
self.data["connection_id"] = connection_id
|
||||
if host_id:
|
||||
self.data["host_id"] = host_id
|
||||
|
||||
|
||||
class SubscriptionData:
|
||||
def __init__(
|
||||
self,
|
||||
subscription_type: Union[EventType, str],
|
||||
condition: Optional[SubscriptionCondition] = None,
|
||||
) -> None:
|
||||
self.data = {
|
||||
"type": subscription_type,
|
||||
"condition": condition.data if condition else None,
|
||||
}
|
||||
|
||||
|
||||
class WebsocketMessage:
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.raw_data = data
|
||||
|
||||
|
||||
class Dispatch(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.type: EventType = data.get("type")
|
||||
self.body: ChangeMap = ChangeMap(data.get("body"))
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
class Hello(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.session_id: str = data.get("session_id")
|
||||
self.heartbeat_interval: int = data.get("heartbeat_interval")
|
||||
self.subscription_limit: int = data.get("subscription_limit")
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
class Heartbeat(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, int]) -> None:
|
||||
self.count: int = data.get("count")
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
class Ack(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.command: str = data.get("command")
|
||||
self.data: Any = data.get("data")
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
class Reconnect(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
class Error(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.message: str = data.get("message")
|
||||
self.fields: dict[str, str] = data.get("fields")
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
class EndOfStream(WebsocketMessage):
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.code: ServerCloseCodes = data.get("code")
|
||||
self.should_reconnect: bool = self.code in ServerCloseCodes.RECONNECT_CODES
|
||||
self.message: Optional[str] = data.get("message")
|
||||
super().__init__(data)
|
||||
|
||||
|
||||
ResponseTypes = Union[Dispatch, Hello, Heartbeat, Ack, Reconnect, Error, EndOfStream]
|
||||
30
eventapi/__init__.py
Normal file
30
eventapi/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from .EventApi import EventApi
|
||||
from .Exceptions import *
|
||||
from .WebSocket import *
|
||||
|
||||
__all__ = [
|
||||
"EventApi",
|
||||
"NoActiveConnectionException",
|
||||
"ConnectionException",
|
||||
"NoActiveSubscriptionsException",
|
||||
"SubscriptionException",
|
||||
"EventApiException",
|
||||
"WebsocketMessageType",
|
||||
"ServerCloseCodes",
|
||||
"EventType",
|
||||
"UserConnection",
|
||||
"User",
|
||||
"ChangeField",
|
||||
"ChangeMap",
|
||||
"SubscriptionCondition",
|
||||
"SubscriptionData",
|
||||
"WebsocketMessage",
|
||||
"Dispatch",
|
||||
"Hello",
|
||||
"Heartbeat",
|
||||
"Ack",
|
||||
"Reconnect",
|
||||
"Error",
|
||||
"EndOfStream",
|
||||
"ResponseTypes",
|
||||
]
|
||||
33
setup.py
Normal file
33
setup.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from pathlib import Path
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
cwd = Path(__file__).parent
|
||||
long_description = (cwd / "README.md").read_text()
|
||||
|
||||
setup(
|
||||
name="eventapi",
|
||||
version="1.0.0",
|
||||
author="yoggys",
|
||||
author_email="yoggies@yoggies.dev",
|
||||
description="Wrapper for 7TV EventAPI.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/yoggys/7tv_eventapi_wrapper",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"websockets ~= 12.0",
|
||||
],
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
],
|
||||
keywords=["python", "unb", "unbelievaboat", "api", "wrapper", "async", "asyncio"],
|
||||
)
|
||||
Reference in New Issue
Block a user