diff --git a/.gitignore b/.gitignore
index ebbd23a..10b5603 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,7 +24,7 @@ _testing.py
# Packaging
*.egg-info
bin
-build
+docs/build
dist
MANIFEST
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..b132679
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,10 @@
+version: 2
+
+sphinx:
+ configuration: docs/source/conf.py
+
+python:
+ version: "3.8"
+ install:
+ - requirements: requirements.txt
+ - requirements: docs-requirements.txt
diff --git a/docs-requirements.txt b/docs-requirements.txt
new file mode 100644
index 0000000..e9bc0cf
--- /dev/null
+++ b/docs-requirements.txt
@@ -0,0 +1,3 @@
+furo
+sphinxcontrib_trio
+sphinx_design
\ No newline at end of file
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d0c3cbf
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..81eb1e9
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,37 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
+
+@pause
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
new file mode 100644
index 0000000..70e8bdb
--- /dev/null
+++ b/docs/source/_static/custom.css
@@ -0,0 +1,46 @@
+.py-attribute-table {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ margin: 0 2em;
+ padding-top: 16px;
+}
+
+.py-attribute-table-column {
+ flex: 1 1 auto;
+}
+
+.py-attribute-table-column:not(:first-child) {
+ margin-top: 1em;
+}
+
+.py-attribute-table-column > span {
+ font-weight: bold;
+}
+
+main .py-attribute-table-column > ul {
+ list-style: none;
+ margin: 4px 0;
+ padding-left: 0;
+ font-size: 0.95em;
+}
+
+.py-attribute-table-entry {
+ margin: 0;
+ padding: 2px 0 2px 0.2em;
+ display: flex;
+ line-height: 1.2em;
+}
+
+.py-attribute-table-entry > a {
+ padding-left: 0.5em;
+ flex-grow: 1;
+}
+.py-attribute-table-badge {
+ flex-basis: 3em;
+ text-align: right;
+ font-size: 0.9em;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
\ No newline at end of file
diff --git a/docs/source/client.rst b/docs/source/client.rst
new file mode 100644
index 0000000..6b18d0c
--- /dev/null
+++ b/docs/source/client.rst
@@ -0,0 +1,16 @@
+.. currentmodule:: melisa
+
+Melisa Module
+=============
+
+Client
+-------
+
+.. attributetable:: Client
+
+.. autoclass:: Client
+ :exclude-members: listen
+ :inherited-members:
+
+ .. automethod:: Client.listen()
+ :decorator:
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..e0172ee
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,75 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+import sys
+import os
+
+sys.path.append(os.path.abspath("../.."))
+sys.path.append(os.path.abspath('extensions'))
+
+
+project = 'Melisa'
+copyright = '2022, MelisaDev'
+author = 'MelisaDev'
+
+# The full version, including alpha/beta/rc tags
+release = '0.0.1'
+
+
+# -- General configuration ---------------------------------------------------
+
+extensions = [
+ 'sphinx_design',
+ "sphinx.ext.napoleon",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.autosectionlabel",
+ "sphinx.ext.extlinks",
+ "sphinxcontrib_trio",
+ "attributable"
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+exclude_patterns = []
+
+intersphinx_mapping = {
+ "py": ("https://docs.python.org/3", None),
+ "aiohttp": ("https://docs.aiohttp.org/en/stable/", None),
+}
+
+
+# -- Options for HTML output -------------------------------------------------
+
+html_theme = 'furo'
+html_theme_options = {
+ "sidebar_hide_name": True,
+}
+pygments_style = 'monokai'
+default_dark_mode = True
+html_static_path = ['_static']
+html_css_files = ["custom.css"]
+
+rst_prolog = """
+.. |coro| replace:: This function is a |coroutine_link|_.
+.. |maybecoro| replace:: This function *could be a* |coroutine_link|_.
+.. |coroutine_link| replace:: *coroutine*
+.. _coroutine_link: https://docs.python.org/3/library/asyncio-task.html#coroutine
+"""
\ No newline at end of file
diff --git a/docs/source/extensions/attributable.py b/docs/source/extensions/attributable.py
new file mode 100644
index 0000000..c20eff1
--- /dev/null
+++ b/docs/source/extensions/attributable.py
@@ -0,0 +1,286 @@
+"""
+I am so sorry, but i found this extension somewhere in the internet.
+IT LOOKS REALLY BEAUTIFUL!
+"""
+
+from sphinx.util.docutils import SphinxDirective
+from sphinx.locale import _
+from docutils import nodes
+from sphinx import addnodes
+
+from collections import OrderedDict, namedtuple
+import importlib
+import inspect
+import re
+
+
+class attributetable(nodes.General, nodes.Element):
+ pass
+
+
+class attributetablecolumn(nodes.General, nodes.Element):
+ pass
+
+
+class attributetabletitle(nodes.TextElement):
+ pass
+
+
+class attributetableplaceholder(nodes.General, nodes.Element):
+ pass
+
+
+class attributetablebadge(nodes.TextElement):
+ pass
+
+
+class attributetable_item(nodes.Part, nodes.Element):
+ pass
+
+
+def visit_attributetable_node(self, node):
+ class_ = node["python-class"]
+ self.body.append(
+ f'
'
+ )
+
+
+def visit_attributetablecolumn_node(self, node):
+ self.body.append(self.starttag(
+ node, 'div', CLASS='py-attribute-table-column')
+ )
+
+
+def visit_attributetabletitle_node(self, node):
+ self.body.append(self.starttag(node, 'span'))
+
+
+def visit_attributetablebadge_node(self, node):
+ attributes = {
+ 'class': 'py-attribute-table-badge',
+ 'title': node['badge-type'],
+ }
+ self.body.append(self.starttag(node, 'span', **attributes))
+
+
+def visit_attributetable_item_node(self, node):
+ self.body.append(self.starttag(
+ node, 'li', CLASS='py-attribute-table-entry')
+ )
+
+
+def depart_attributetable_node(self, node):
+ self.body.append('
')
+
+
+def depart_attributetablecolumn_node(self, node):
+ self.body.append('')
+
+
+def depart_attributetabletitle_node(self, node):
+ self.body.append('')
+
+
+def depart_attributetablebadge_node(self, node):
+ self.body.append('')
+
+
+def depart_attributetable_item_node(self, node):
+ self.body.append('')
+
+
+_name_parser_regex = re.compile(r'(?P[\w.]+\.)?(?P\w+)')
+
+
+class PyAttributeTable(SphinxDirective):
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def parse_name(self, content):
+ path, name = _name_parser_regex.match(content).groups()
+ if path:
+ modulename = path.rstrip('.')
+ else:
+ modulename = self.env.temp_data.get('autodoc:module')
+ if not modulename:
+ modulename = self.env.ref_context.get('py:module')
+ if modulename is None:
+ raise RuntimeError('modulename somehow None for %s in %s.' % (
+ content, self.env.docname))
+
+ return modulename, name
+
+ def run(self):
+ """If you're curious on the HTML this is meant to generate:
+
+ However, since this requires the tree to be complete
+ and parsed, it'll need to be done at a different stage and then
+ replaced.
+ """
+ content = self.arguments[0].strip()
+ node = attributetableplaceholder('')
+ modulename, name = self.parse_name(content)
+ node['python-doc'] = self.env.docname
+ node['python-module'] = modulename
+ node['python-class'] = name
+ node['python-full-name'] = f'{modulename}.{name}'
+ return [node]
+
+
+def build_lookup_table(env):
+ # Given an environment, load up a lookup table of
+ # full-class-name: objects
+ result = {}
+ domain = env.domains['py']
+
+ ignored = {
+ 'data', 'exception', 'module', 'class',
+ }
+
+ for (fullname, _, objtype, docname, _, _) in domain.get_objects():
+ if objtype in ignored:
+ continue
+
+ classname, _, child = fullname.rpartition('.')
+ try:
+ result[classname].append(child)
+ except KeyError:
+ result[classname] = [child]
+
+ return result
+
+
+TableElement = namedtuple('TableElement', 'fullname label badge')
+
+
+def process_attributetable(app, doctree, fromdocname):
+ env = app.builder.env
+
+ lookup = build_lookup_table(env)
+ for node in doctree.traverse(attributetableplaceholder):
+ modulename, classname, fullname = node['python-module'], node['python-class'], node['python-full-name']
+ groups = get_class_results(lookup, modulename, classname, fullname)
+ table = attributetable('')
+ for label, subitems in groups.items():
+ if not subitems:
+ continue
+ table.append(class_results_to_node(
+ label, sorted(subitems, key=lambda c: c.label)))
+
+ table['python-class'] = fullname
+
+ node.replace_self([table] if table else [])
+
+
+def get_class_results(lookup, modulename, name, fullname):
+ module = importlib.import_module(modulename)
+ cls = getattr(module, name)
+
+ groups = OrderedDict([
+ (_('Attributes'), []),
+ (_('Methods'), []),
+ ])
+
+ try:
+ members = lookup[fullname]
+ except KeyError:
+ return groups
+
+ for attr in members:
+ attrlookup = f'{fullname}.{attr}'
+ key = _('Attributes')
+ badge = None
+ label = attr
+ value = None
+
+ for base in cls.__mro__:
+ value = base.__dict__.get(attr)
+ if value is not None:
+ break
+
+ if value is not None:
+ doc = value.__doc__ or ''
+ if inspect.iscoroutinefunction(value) or doc.startswith('|coro|'):
+ key = _('Methods')
+ badge = attributetablebadge('async', 'async')
+ badge['badge-type'] = _('coroutine')
+ elif isinstance(value, classmethod):
+ key = _('Methods')
+ label = f'{name}.{attr}'
+ badge = attributetablebadge('cls', 'cls')
+ badge['badge-type'] = _('classmethod')
+ elif (
+ inspect.isfunction(value)
+ or isinstance(value, staticmethod)
+ ):
+ if (
+ doc.startswith(('A decorator', 'A shortcut decorator'))
+ or label in ("event", "loop")
+ ):
+ # finicky but surprisingly consistent
+ badge = attributetablebadge('@', '@')
+ badge['badge-type'] = _('decorator')
+ key = _('Methods')
+ else:
+ key = _('Methods')
+ badge = attributetablebadge('def', 'def')
+ badge['badge-type'] = _('method')
+
+ groups[key].append(TableElement(
+ fullname=attrlookup, label=label, badge=badge))
+
+ return groups
+
+
+def class_results_to_node(key, elements):
+ title = attributetabletitle(key, key)
+ ul = nodes.bullet_list('')
+ for element in elements:
+ ref = nodes.reference('', '', internal=True,
+ refuri='#' + element.fullname,
+ anchorname='',
+ *[nodes.Text(element.label)])
+ para = addnodes.compact_paragraph('', '', ref)
+ if element.badge is not None:
+ ul.append(attributetable_item('', element.badge, para))
+ else:
+ ul.append(attributetable_item('', para))
+
+ return attributetablecolumn('', title, ul)
+
+
+def setup(app):
+ app.add_directive('attributetable', PyAttributeTable)
+ app.add_node(attributetable, html=(
+ visit_attributetable_node, depart_attributetable_node))
+ app.add_node(attributetablecolumn, html=(
+ visit_attributetablecolumn_node, depart_attributetablecolumn_node))
+ app.add_node(attributetabletitle, html=(
+ visit_attributetabletitle_node, depart_attributetabletitle_node))
+ app.add_node(attributetablebadge, html=(
+ visit_attributetablebadge_node, depart_attributetablebadge_node))
+ app.add_node(attributetable_item, html=(
+ visit_attributetable_item_node, depart_attributetable_item_node))
+ app.add_node(attributetableplaceholder)
+ app.connect('doctree-resolved', process_attributetable)
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..8964a24
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,15 @@
+Welcome to Melisa's documentation!
+==================================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Documentation:
+
+ client
+ models/index
+
+
+Indices and tables
+==================
+
+* :ref:`search`
diff --git a/docs/source/models/index.rst b/docs/source/models/index.rst
new file mode 100644
index 0000000..24a02de
--- /dev/null
+++ b/docs/source/models/index.rst
@@ -0,0 +1,7 @@
+Melisa Models
+=====================
+
+.. toctree::
+ :maxdepth: 1
+
+ user
\ No newline at end of file
diff --git a/docs/source/models/user.rst b/docs/source/models/user.rst
new file mode 100644
index 0000000..c9cf9df
--- /dev/null
+++ b/docs/source/models/user.rst
@@ -0,0 +1,30 @@
+.. currentmodule:: melisa.models
+
+Melisa Models User
+===========================
+
+User
+----
+
+PremiumTypes
+~~~~~~~~~~~~~
+
+.. autoclass:: PremiumTypes()
+
+UserFlags
+~~~~~~~~~~
+
+.. autoclass:: UserFlags()
+
+VisibilityTypes
+~~~~~~~~~~~~~~~~
+
+.. autoclass:: VisibilityTypes()
+
+User
+~~~~~
+
+.. attributetable:: User
+
+.. autoclass:: User()
+
diff --git a/melisa/client.py b/melisa/client.py
index 9b60193..a29e2d4 100644
--- a/melisa/client.py
+++ b/melisa/client.py
@@ -11,7 +11,33 @@ from typing import Dict, List, Union
class Client:
- def __init__(self, token, intents, **kwargs):
+ """
+ This is the main instance which is between the programmer and the Discord API.
+
+ This Client represents your bot.
+
+ Parameters
+ ----------
+ token : :class:`str`
+ The token to login (you can found it in the developer portal)
+ intents : :class:`~objects.app.intents.Intents`
+ The Discord Intents values.
+ activity : :class:`~models.user.presence.BotActivity`
+ The Activity to set (on connecting)
+ status : :class:`str`
+ The Status to set (on connecting). Can be generated using :class:`~models.user.presence.StatusType`
+
+ Attributes
+ ----------
+ user: :class:`~models.user.user.User`
+ The user object of the client
+ http: :class:`~core.http.HTTPClient`
+ HTTP client for the http-requests to the Discord API
+ shards: :class:`Dict[int, Shard]`
+ Bot's shards.
+ """
+
+ def __init__(self, token, intents, *, activity=None, status: str = None):
self.shards: Dict[int, Shard] = {}
self.http = HTTPClient(token)
self._events = {}
@@ -19,26 +45,26 @@ class Client:
self.guilds = []
self.user = None
- self.loop = asyncio.get_event_loop()
+ self._loop = asyncio.get_event_loop()
- self._gateway_info = self.loop.run_until_complete(self._get_gateway())
+ self._gateway_info = self._loop.run_until_complete(self._get_gateway())
self.intents = intents
self._token = token
- self._activity = kwargs.get("activity")
- self._status = kwargs.get("status")
+ self._activity = activity
+ self._status = status
async def _get_gateway(self):
"""Get Gateway information"""
return GatewayBotInfo.from_dict(await self.http.get("gateway/bot"))
def listen(self, callback: Coro):
- """Method to set the listener.
+ """Method or Decorator to set the listener.
Parameters
----------
- callback (:obj:`function`)
+ callback : :class:`melisa.utils.types.Coro`
Coroutine Callback Function
"""
if not asyncio.iscoroutinefunction(callback):
@@ -53,8 +79,8 @@ class Client:
"""
inited_shard = Shard(self, 0, 1)
- asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
- self.loop.run_forever()
+ asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
+ self._loop.run_forever()
def run_shards(self, num_shards: int, *, shard_ids: List[int] = None):
"""
@@ -64,9 +90,6 @@ class Client:
----------
num_shards : :class:`int`
The endpoint to send the request to.
-
- Keyword Arguments:
-
shard_ids: Optional[:class:`List[int]`]
List of Ids of shards to start.
"""
@@ -76,8 +99,8 @@ class Client:
for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards)
- asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
- self.loop.run_forever()
+ asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
+ self._loop.run_forever()
def run_autosharded(self):
"""
@@ -89,8 +112,8 @@ class Client:
for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards)
- asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
- self.loop.run_forever()
+ asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
+ self._loop.run_forever()
async def fetch_user(self, user_id: Union[Snowflake, str, int]):
"""
diff --git a/melisa/core/gateway.py b/melisa/core/gateway.py
index b891e2c..8345f6f 100644
--- a/melisa/core/gateway.py
+++ b/melisa/core/gateway.py
@@ -95,13 +95,12 @@ class Gateway:
await self.resume()
async def check_heartbeating(self):
- await asyncio.sleep(20)
+ while True:
+ await asyncio.sleep(20)
- if self._last_send + 60.0 < time.perf_counter():
- await self.ws.close(code=4000)
- await self.handle_close(4000)
-
- await self.check_heartbeating()
+ if self._last_send + 60.0 < time.perf_counter():
+ await self.ws.close(code=4000)
+ await self.handle_close(4000)
async def send(self, payload: str) -> None:
await self.ws.send_str(payload)
diff --git a/melisa/models/user/user.py b/melisa/models/user/user.py
index 4fe02ac..21830d7 100644
--- a/melisa/models/user/user.py
+++ b/melisa/models/user/user.py
@@ -111,36 +111,40 @@ class User(APIObjectBase):
Attributes
----------
- id:
+ id: :class:`~melisa.utils.types.Snowflake`
the user's id
- username:
+ username: :class:`str`
the user's username, not unique across the platform
- discriminator:
+ discriminator: :class:`int`
the user's 4-digit discord-tag
- avatar:
+ avatar: :class:`str`
the user's avatar hash
- bot:
+ bot: :class:`bool`
whether the user belongs to an OAuth2 application
- system:
+ system: :class:`bool`
whether the user is an Official Discord System user (part of the urgent message system)
- mfa_enabled:
+ mfa_enabled: :class:`bool`
whether the user has two factor enabled on their account
- banner:
+ banner: :class:`str`
the user's banner hash
- accent_color:
+ accent_color: :class:`int`
the user's banner color encoded as an integer representation of hexadecimal color code
- locale:
+ locale: :class:`str`
the user's chosen language option
- verified:
+ verified: :class:`bool`
whether the email on this account has been verified
- email:
+ email: :class:`str`
the user's email
- flags:
+ flags: :class:`~models.user.user.UserFlags`
the flags on a user's account
- premium_type:
+ premium_type: :class:`int`
the type of Nitro subscription on a user's account
- public_flags:
+ public_flags: :class:`int`
the public flags on a user's account
+ premium: :class:`PremiumTypes`
+ The user their premium type in a usable enum.
+ mention: :class:`str`
+ The user's mention string. (<@id>)
"""
id: APINullable[Snowflake] = None
@@ -187,10 +191,6 @@ class User(APIObjectBase):
""":class:`str`: The user's mention string. (<@id>)"""
return "<@{}>".format(self.id)
- @property
- def avatar_url(self):
- """Avatar url (from the discord cdn server)"""
- return (
- "https://cdn.discordapp.com/avatars/{}/{}.png".format(self.id, self.avatar),
- "?size=1024"
- )
+ def avatar_url(self) -> str:
+ """Avatar url (from the Discord CDN server)"""
+ return "https://cdn.discordapp.com/avatars/{}/{}.png?size=1024".format(self.id, self.avatar)