some requirements and tests

This commit is contained in:
grey-cat-1908 2022-03-18 14:57:47 +03:00
parent 9036393198
commit d53b349e95
22 changed files with 120 additions and 78 deletions

5
.flake8 Normal file
View file

@ -0,0 +1,5 @@
[flake8]
ignore = D203, H501, F403, F401, C901, W503
exclude = .git, .idea, __pycache__, docs, build, dev, dist, venv, .history, .github, examples
max-complexity = 10
max-line-length = 100

1
.gitignore vendored
View file

@ -19,6 +19,7 @@ tools/*
# Dev testing # Dev testing
dev dev
.pytest_cache
_testing.py _testing.py
# Packaging # Packaging

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 - Present MelisaDev
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.

View file

@ -1 +1 @@
from .client import Client from .client import *

View file

@ -25,7 +25,8 @@ class Client:
activity : :class:`~models.user.presence.BotActivity` activity : :class:`~models.user.presence.BotActivity`
The Activity to set (on connecting) The Activity to set (on connecting)
status : :class:`str` status : :class:`str`
The Status to set (on connecting). Can be generated using :class:`~models.user.presence.StatusType` The Status to set (on connecting).
Can be generated using :class:`~models.user.presence.StatusType`
Attributes Attributes
---------- ----------
@ -82,7 +83,8 @@ class Client:
""" """
inited_shard = Shard(self, 0, 1) inited_shard = Shard(self, 0, 1)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop) asyncio.ensure_future(inited_shard.launch(activity=self._activity,
status=self._status), loop=self._loop)
self._loop.run_forever() self._loop.run_forever()
def run_shards(self, num_shards: int, *, shard_ids: List[int] = None): def run_shards(self, num_shards: int, *, shard_ids: List[int] = None):
@ -102,7 +104,8 @@ class Client:
for shard_id in shard_ids: for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards) inited_shard = Shard(self, shard_id, num_shards)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop) asyncio.ensure_future(inited_shard.launch(activity=self._activity,
status=self._status), loop=self._loop)
self._loop.run_forever() self._loop.run_forever()
def run_autosharded(self): def run_autosharded(self):
@ -115,7 +118,8 @@ class Client:
for shard_id in shard_ids: for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards) inited_shard = Shard(self, shard_id, num_shards)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop) asyncio.ensure_future(inited_shard.launch(activity=self._activity,
status=self._status), loop=self._loop)
self._loop.run_forever() self._loop.run_forever()
async def fetch_user(self, user_id: Union[Snowflake, str, int]): async def fetch_user(self, user_id: Union[Snowflake, str, int]):

View file

@ -73,20 +73,21 @@ class Gateway:
"intents": self.intents, "intents": self.intents,
"properties": { "properties": {
"$os": sys.platform, "$os": sys.platform,
"$browser": "melisa", "$browser": "Melisa Python Library",
"$device": "melisa" "$device": "Melisa Python Library"
}, },
"compress": True, "compress": True,
"shard": [shard_id, num_shards], "shard": [shard_id, num_shards],
"presence": self.generate_presence(kwargs.get("start_activity"), kwargs.get("start_status")) "presence": self.generate_presence(kwargs.get("start_activity"),
} kwargs.get("start_status"))}
self._zlib: zlib._Decompress = zlib.decompressobj() self._zlib: zlib._Decompress = zlib.decompressobj()
self._buffer: bytearray = bytearray() self._buffer: bytearray = bytearray()
async def connect(self) -> None: async def connect(self) -> None:
self.ws = await self.__session.ws_connect( self.ws = await self.__session.ws_connect(
f'wss://gateway.discord.gg/?v={self.GATEWAY_VERSION}&encoding=json&compress=zlib-stream') f'wss://gateway.discord.gg/?v={self.GATEWAY_VERSION}&encoding=json&compress=zlib-stream'
)
if self.session_id is None: if self.session_id is None:
await self.send_identify() await self.send_identify()

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import Dict, Optional, Union, Any from typing import Dict, Optional
from aiohttp import ClientSession, ClientResponse from aiohttp import ClientSession, ClientResponse
@ -15,14 +15,16 @@ from melisa.exceptions import (NotModifiedError,
class HTTPClient: class HTTPClient:
API_VERSION = 9
def __init__(self, token: str, *, ttl: int = 5): def __init__(self, token: str, *, ttl: int = 5):
self.url: str = f"https://discord.com/api/v9" self.url: str = f"https://discord.com/api/v{self.API_VERSION}"
self.max_ttl: int = ttl self.max_ttl: int = ttl
headers: Dict[str, str] = { headers: Dict[str, str] = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bot {token}", "Authorization": f"Bot {token}",
"User-Agent": f"Melisa Python Library" "User-Agent": "Melisa Python Library"
} }
self.__http_exceptions: Dict[int, HTTPException] = { self.__http_exceptions: Dict[int, HTTPException] = {

View file

@ -14,7 +14,8 @@ class LoginFailure(ClientException):
class ConnectionClosed(ClientException): class ConnectionClosed(ClientException):
"""Exception that's thrown when the gateway connection is closed for reasons that could not be handled """Exception that's thrown when the gateway connection is closed
for reasons that could not be handled
internally. """ internally. """
def __init__(self, socket, *, shard_id, code=None): def __init__(self, socket, *, shard_id, code=None):
@ -26,14 +27,16 @@ class ConnectionClosed(ClientException):
class PrivilegedIntentsRequired(ClientException): class PrivilegedIntentsRequired(ClientException):
"""Occurs when the gateway requests privileged intents, but they are not yet marked on the developer page. """Occurs when the gateway requests privileged intents,
but they are not yet marked on the developer page.
Visit to https://discord.com/developers/applications/ Visit to https://discord.com/developers/applications/
""" """
def __init__(self, shard_id): def __init__(self, shard_id):
self.shard_id = shard_id self.shard_id = shard_id
message = "Shard ID {} is requesting privileged intents that have not been explicitly enabled in the " \ message = "Shard ID {} is requesting privileged intents " \
"that have not been explicitly enabled in the " \
"developer portal. Please visit to https://discord.com/developers/applications/ " "developer portal. Please visit to https://discord.com/developers/applications/ "
super().__init__(message.format(self.shard_id)) super().__init__(message.format(self.shard_id))

View file

@ -1,5 +1,3 @@
from .app import * from .app import *
from .guild import * from .guild import *
from .user import * from .user import *

View file

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
from asyncio import create_task, Task, sleep from asyncio import create_task, sleep
from typing import Optional
from ...core.gateway import Gateway from ...core.gateway import Gateway
from ..user import BotActivity from ..user import BotActivity
@ -28,7 +27,8 @@ class Shard:
@property @property
def latency(self) -> float: def latency(self) -> float:
""":class:`float`: """:class:`float`:
Measures latency between a HEARTBEAT command and a HEARTBEAT_ACK event in seconds for this shard""" Measures latency between a HEARTBEAT command
and a HEARTBEAT_ACK event in seconds for this shard"""
return self._gateway.latency return self._gateway.latency
async def launch(self, **kwargs) -> Shard: async def launch(self, **kwargs) -> Shard:

View file

@ -1,10 +1 @@
from __future__ import annotations from __future__ import annotations
from enum import IntEnum
from dataclasses import dataclass
from typing import Optional
from ...utils.api_model import APIModelBase
from ...utils.types import APINullable
from ...utils.snowflake import Snowflake

View file

@ -1,7 +0,0 @@
from ...utils import Snowflake
class Guild:
pass

View file

@ -24,13 +24,15 @@ class ActivityType(IntEnum):
GAME: GAME:
Playing {name} (Playing Rocket League) Playing {name} (Playing Rocket League)
STREAMING: STREAMING:
Streaming {details} (Streaming Rocket League) It supports only YouTube and Twitch Streaming {details} (Streaming Rocket League)
It supports only YouTube and Twitch
LISTENING: LISTENING:
Listening to {name} (Listening to Spotify) Listening to {name} (Listening to Spotify)
WATCHING: WATCHING:
Watching {name} (Watching YouTube Together) Watching {name} (Watching YouTube Together)
CUSTOM: CUSTOM:
{emoji} {name} (":smiley: I am cool") (THIS ACTIVITY IS NOT SUPPORTED FOR BOTS) {emoji} {name} (":smiley: I am cool")
(THIS ACTIVITY IS NOT SUPPORTED FOR BOTS)
COMPETING: COMPETING:
Competing in {name} (Competing in Arena World Champions) Competing in {name} (Competing in Arena World Champions)
""" """
@ -100,11 +102,13 @@ class ActivityAssets(BasePresence, APIModelBase):
Attributes Attributes
---------- ----------
large_image: Optional[:class:`str`] large_image: Optional[:class:`str`]
(Large Image) Activity asset images are arbitrary strings which usually contain snowflake IDs (Large Image) Activity asset images are arbitrary strings
which usually contain snowflake IDs
large_text: Optional[:class:`str`] large_text: Optional[:class:`str`]
text displayed when hovering over the large image of the activity text displayed when hovering over the large image of the activity
small_image: Optional[:class:`str`] small_image: Optional[:class:`str`]
(Small Image) Activity asset images are arbitrary strings which usually contain snowflake IDs (Small Image) Activity asset images are arbitrary strings
which usually contain snowflake IDs
small_text: Optional[:class:`str`] small_text: Optional[:class:`str`]
text displayed when hovering over the small image of the activity text displayed when hovering over the small image of the activity
""" """
@ -153,8 +157,10 @@ class ActivityFlags(BasePresence, APIModelBase):
@dataclass(repr=False) @dataclass(repr=False)
class ActivityButton(BasePresence, APIModelBase): class ActivityButton(BasePresence, APIModelBase):
"""When received over the gateway, the buttons field is an array of strings, which are the button labels. Bots """When received over the gateway, the buttons field is an array of strings,
cannot access a user's activity button URLs. When sending, the buttons field must be an array of the below which are the button labels. Bots
cannot access a user's activity button URLs.
When sending, the buttons field must be an array of the below
object: object:
Attributes Attributes
---------- ----------

View file

@ -199,5 +199,4 @@ class User(APIModelBase):
# ToDo: Add docstrings # ToDo: Add docstrings
# ToDo: Add checking this channel in cache # ToDo: Add checking this channel in cache
return await self._http.post( return await self._http.post(
"/users/@me/channels", data={"recipient_id": self.id} "/users/@me/channels", data={"recipient_id": self.id})
)

View file

@ -3,11 +3,13 @@ from __future__ import annotations
class Snowflake(int): class Snowflake(int):
""" """
Discord utilizes Twitter's snowflake format for uniquely identifiable descriptors (IDs). These IDs are guaranteed Discord utilizes Twitter's snowflake format for uniquely identifiable descriptors (IDs).
to be unique across all of Discord, except in some unique scenarios in which child objects share their parent's These IDs are guaranteed to be unique across all of Discord,
ID. Because Snowflake IDs are up to 64 bits in size (e.g. a uint64), they are always returned as strings in the except in some unique scenarios in which child objects share their parent's ID.
HTTP API to prevent integer overflows in some languages. See Gateway ETF/JSON for more information regarding Because Snowflake IDs are up to 64 bits in size (e.g. a uint64),
Gateway encoding. they are always returned as strings in the HTTP API
to prevent integer overflows in some languages.
See Gateway ETF/JSON for more information regarding Gateway encoding.
Read more here: https://discord.com/developers/docs/reference#snowflakes Read more here: https://discord.com/developers/docs/reference#snowflakes
""" """

2
packages/dev.txt Normal file
View file

@ -0,0 +1,2 @@
flake8==4.0.0
pytest==6.2.5

0
tests/__init__.py Normal file
View file

2
tests/requirements.txt Normal file
View file

@ -0,0 +1,2 @@
pytest
pytest-asyncio

12
tests/test_snowflakes.py Normal file
View file

@ -0,0 +1,12 @@
from melisa.utils import Snowflake
class TestSnowflakes:
def test_assertions(self):
assert Snowflake(2) == 2
assert Snowflake("2") == 2
def test_timestamps(self):
sflake = Snowflake(175928847299117063)
assert sflake.timestamp == 41944705796
assert sflake.unix == 1462015105796