From af70bf36eca6e6109696d8395d673f08b75395c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Ro=C5=BCnawski?= <48837433+roznawsk@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:00:29 +0200 Subject: [PATCH] Add docs (#4) * Update readme * generate docs * github pages * Fix tests * fix typo * Fix * Fix lint * impove naming * Cleanup ci job * switch to main branch * docs title * Default server address and token * Add docstrings * Switch to main * Update jellyfish/_room_api.py Co-authored-by: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> --------- Co-authored-by: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> --- .circleci/config.yml | 5 +- .github/workflows/docs.yml | 35 +++ .gitignore | 4 +- README.md | 36 ++- requirements.txt => dev-requirements.txt | 3 +- doc_templates/module.html.jinja2 | 298 +++++++++++++++++++++++ docker-compose-test.yaml | 2 +- jellyfish/__init__.py | 12 +- jellyfish/_room_api.py | 37 ++- pdoc.sh | 6 + pylint.sh | 2 +- pylintrc | 2 +- pyproject.toml | 2 +- tests/test_room_api.py | 13 +- 14 files changed, 430 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/docs.yml rename requirements.txt => dev-requirements.txt (82%) create mode 100644 doc_templates/module.html.jinja2 create mode 100755 pdoc.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 7295084..23b2ead 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,8 +17,9 @@ jobs: executor: python/default steps: - checkout - - python/install-packages: - pkg-manager: pip + - run: + name: Install python dependencies + command: pip install -r dev-requirements.txt - run: name: Lint command: ./pylint.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..99f2dfe --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: docs + +on: + push: + branches: + - main + +jobs: + # Build the documentation and upload the static HTML files as an artifact. + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.8' + - run: pip install -r dev-requirements.txt + - run: ./pdoc.sh + - uses: actions/upload-pages-artifact@v2 + with: + path: doc + + # Deploy the artifact to GitHub pages. + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v2 diff --git a/.gitignore b/.gitignore index 68bc17f..6f19aa6 100644 --- a/.gitignore +++ b/.gitignore @@ -68,8 +68,8 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ +# pdoc documentation +/doc # PyBuilder .pybuilder/ diff --git a/README.md b/README.md index 58c0e51..8365370 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,45 @@ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jellyfish-dev/python-server-sdk/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/jellyfish-dev/python-server-sdk/tree/main) -Python server SDK for [Jellyfish](https://github.com/jellyfish-dev/jellyfish). +Python server SDK for the [Jellyfish](https://github.com/jellyfish-dev/jellyfish) media server. + +Read the docs [here](https://jellyfish-dev.github.io/python-server-sdk/jellyfish.html) ## Installation -TODO + +You can install the latest version of the package from github: +``` +pip install git+https://github.com/jellyfish-dev/python-server-sdk +``` ## Usage +First create a `RoomApi` instance, providing the jellyfish server address and api token + +```python +from jellyfish import RoomApi + +room_api = RoomApi(server_address='localhost:5002', server_api_token='development') +``` + +You can use it to interact with Jellyfish managing rooms, peers and components + +```python +# Create a room +jellyfish_address, room = room_api.create_room() +# 'localhost:5002', Room(components=[], id='f7cc2eac-f699-4609-ac8f-92f1ad6bea0c', peers=[]) + +# Add peer to the room +from jellyfish import PeerOptionsWebRTC + +peer_token, peer_webrtc = room_api.add_peer(room.id, peer_type='webrtc', options=PeerOptionsWebRTC()) +# 'AgDYfrCSigFiAA', Peer(id='2869fb5', status=, type='webrtc') + +# Add component to the room +component_hls = room_api.add_component(room.id, component_type='hls') +# Component(id='4c028a86', metadata=ComponentMetadata(playable=False), type='hls') +``` + ## Copyright and License Copyright 2023, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=jellyfish) diff --git a/requirements.txt b/dev-requirements.txt similarity index 82% rename from requirements.txt rename to dev-requirements.txt index ffb4ad9..59d2e75 100644 --- a/requirements.txt +++ b/dev-requirements.txt @@ -5,4 +5,5 @@ python_dateutil==2.8.2 setuptools==67.6.1 typing_extensions==4.7.1 urllib3==2.0.4 -pylint==2.17.5 \ No newline at end of file +pylint==2.17.5 +pdoc==14.1.0 diff --git a/doc_templates/module.html.jinja2 b/doc_templates/module.html.jinja2 new file mode 100644 index 0000000..fbe9e10 --- /dev/null +++ b/doc_templates/module.html.jinja2 @@ -0,0 +1,298 @@ +{% extends "frame.html.jinja2" %} +{% block title %}Jellyfish Python Server SDK{% endblock %} +{% block nav %} + {% block module_list_link %} + {% set parentmodule = ".".join(module.modulename.split(".")[:-1]) %} + {% if parentmodule and parentmodule in all_modules %} + + {% include "resources/box-arrow-in-left.svg" %} +   + {{- parentmodule -}} + + {% elif not root_module_name %} + + {% include "resources/box-arrow-in-left.svg" %} +   + Module Index + + {% endif %} + {% endblock %} + + {% block nav_title %} + {% if logo %} + {% if logo_link %}{% endif %} + + {% if logo_link %}{% endif %} + {% endif %} + {% endblock %} + + {% block search_box %} + {% if search and all_modules|length > 1 %} + {# we set a pattern here so that we can use the :valid CSS selector #} + + {% endif %} + {% endblock %} + + {% block nav_index %} + {% set index = module.docstring | to_markdown | to_html | attr("toc_html") %} + {% if index %} +

Contents

+ {{ index | safe }} + {% endif %} + {% endblock %} + + {% block nav_submodules %} + {% if module.submodules %} +

Submodules

+ + {% endif %} + {% endblock %} + + {% block nav_members %} + {% if module.members %} +

API Documentation

+ {{ nav_members(module.members.values()) }} + {% endif %} + {% endblock %} + + {% block nav_footer %} + {% if footer_text %} + + {% endif %} + {% endblock %} + + {% block attribution %} + + built with pdocpdoc logo + + {% endblock %} +{% endblock nav %} +{% block content %} +
+ {% block module_info %} +
+ {% block edit_button %} + {% if edit_url %} + {% if "github.com" in edit_url %} + {% set edit_text = "Edit on GitHub" %} + {% elif "gitlab" in edit_url %} + {% set edit_text = "Edit on GitLab" %} + {% else %} + {% set edit_text = "Edit Source" %} + {% endif %} + {{ edit_text }} + {% endif %} + {% endblock %} + {{ module_name() }} + {{ docstring(module) }} + {{ view_source_state(module) }} + {{ view_source_button(module) }} + {{ view_source_code(module) }} +
+ {% endblock %} + {% block module_contents %} + {% for m in module.flattened_own_members if is_public(m) | trim %} +
+ {{ member(m) }} + {% if m.kind == "class" %} + {% for m in m.own_members if m.kind != "class" and is_public(m) | trim %} +
+ {{ member(m) }} +
+ {% endfor %} + {% set inherited_members = inherited(m) | trim %} + {% if inherited_members %} +
+
Inherited Members
+
+ {{ inherited_members }} +
+
+ {% endif %} + {% endif %} +
+ {% endfor %} + {% endblock %} +
+ {% if mtime %} + {% include "livereload.html.jinja2" %} + {% endif %} + {% block search_js %} + {% if search and all_modules|length > 1 %} + {% include "search.html.jinja2" %} + {% endif %} + {% endblock %} +{% endblock content %} +{# +End of content, beginning of helper macros. +See https://pdoc.dev/docs/pdoc/render_helpers.html#DefaultMacroExtension for an explanation of defaultmacro. +#} +{% defaultmacro bases(cls) %} + {%- if cls.bases -%} + ( + {%- for base in cls.bases -%} + {{ base[:2] | link(text=base[2]) }} + {%- if loop.nextitem %}, {% endif %} + {%- endfor -%} + ) + {%- endif -%} +{% enddefaultmacro %} +{% defaultmacro default_value(var) -%} + {%- if var.default_value_str %} + = + {% if var.default_value_str | length > 100 -%} + + + {%- endif -%} + {{ var.default_value_str | escape | linkify }} + {%- endif -%} +{% enddefaultmacro %} +{% defaultmacro annotation(var) %} + {%- if var.annotation_str -%} + {{ var.annotation_str | escape | linkify }} + {%- endif -%} +{% enddefaultmacro %} +{% defaultmacro decorators(doc) %} + {% for d in doc.decorators if not d.startswith("@_") %} +
{{ d }}
+ {% endfor %} +{% enddefaultmacro %} +{% defaultmacro function(fn) -%} + {{ decorators(fn) }} + {% if fn.name == "__init__" %} + {{ ".".join(fn.qualname.split(".")[:-1]) }} + {{- fn.signature_without_self | format_signature(colon=False) | linkify }} + {% else %} + {{ fn.funcdef }} + {{ fn.name }} + {{- fn.signature | format_signature(colon=True) | linkify }} + {% endif %} +{% enddefaultmacro %} +{% defaultmacro variable(var) -%} + {{ var.name }}{{ annotation(var) }}{{ default_value(var) }} +{% enddefaultmacro %} +{% defaultmacro submodule(mod) -%} + {{ mod.taken_from | link }} +{% enddefaultmacro %} +{% defaultmacro class(cls) -%} + {{ decorators(cls) }} + class + {{ cls.qualname }} + {{- bases(cls) -}}: +{% enddefaultmacro %} +{% defaultmacro member(doc) %} + {{- view_source_state(doc) -}} +
+ {% if doc.kind == "class" %} + {{ class(doc) }} + {% elif doc.kind == "function" %} + {{ function(doc) }} + {% elif doc.kind == "module" %} + {{ submodule(doc) }} + {% else %} + {{ variable(doc) }} + {% endif %} + {{ view_source_button(doc) }} +
+ + {{ view_source_code(doc) }} + {{ docstring(doc) }} +{% enddefaultmacro %} +{% defaultmacro docstring(var) %} + {% if var.docstring %} +
{{ var.docstring | to_markdown | to_html | linkify(namespace=var.qualname) }}
+ {% endif %} +{% enddefaultmacro %} +{% defaultmacro nav_members(members) %} + +{% enddefaultmacro %} +{% defaultmacro is_public(doc) %} + {# + This macro is a bit unconventional in that its output is not rendered, but treated as a boolean: + Returning no text is interpreted as false, returning any other text is iterpreted as true. + Implementing this as a macro makes it very easy to override with a custom template, see + https://github.com/mitmproxy/pdoc/tree/main/examples/custom-template. + #} + {% if not include_undocumented and not doc.docstring %} + {# hide members that are undocumented if include_undocumented has been toggled off. #} + {% elif doc.docstring and "@private" in doc.docstring %} + {# hide members explicitly marked as @private #} + {% elif doc.name == "__init__" and (doc.docstring or (doc.kind == "function" and doc.signature_without_self.parameters)) %} + {# show constructors that have a docstring or at least one extra argument #} + true + {% elif doc.name == "__doc__" %} + {# We don't want to document __doc__ itself, https://github.com/mitmproxy/pdoc/issues/235 #} + {% elif doc.kind == "variable" and doc.is_typevar and not doc.docstring %} + {# do not document TypeVars, that only clutters the docs. #} + {% elif doc.kind == "module" and doc.fullname not in all_modules %} + {# Skip modules that were manually excluded, https://github.com/mitmproxy/pdoc/issues/334 #} + {% elif (doc.qualname or doc.name) is in(module.obj.__all__ or []) %} + {# members starting with an underscore are still public if mentioned in __all__ #} + true + {% elif not doc.name.startswith("_") %} + {# members not starting with an underscore are considered public by default #} + true + {% endif %} +{% enddefaultmacro %} +{# fmt: off #} +{% defaultmacro inherited(cls) %} + {% for base, members in cls.inherited_members.items() %} + {% set m = None %}{# workaround for https://github.com/pallets/jinja/issues/1427 #} + {% set member_html %} + {% for m in members if is_public(m) | trim %} +
+ {{- m.taken_from | link(text=m.name.replace("__init__",base[1])) -}} +
+ {% endfor %} + {% endset %} + {# we may not have any public members, in which case we don't want to print anything. #} + {% if member_html %} +
{{ base | link }}
+ {{ member_html }} +
+ {% endif %} + {% endfor %} +{% enddefaultmacro %} +{# fmt: on #} +{% defaultmacro view_source_state(doc) %} + {% if show_source and doc.source %} + + {% endif %} +{% enddefaultmacro %} +{% defaultmacro view_source_button(doc) %} + {% if show_source and doc.source %} + + {% endif %} +{% enddefaultmacro %} +{% defaultmacro view_source_code(doc) %} + {% if show_source and doc.source %} + {{ doc | highlight }} + {% endif %} +{% enddefaultmacro %} +{% defaultmacro module_name() %} +{% enddefaultmacro %} diff --git a/docker-compose-test.yaml b/docker-compose-test.yaml index 1351a85..a1a69fe 100644 --- a/docker-compose-test.yaml +++ b/docker-compose-test.yaml @@ -31,7 +31,7 @@ services: test: image: python:3.8-alpine3.18 - command: sh -c "cd app/ && pip install -r requirements.txt && pytest" + command: sh -c "cd app/ && pip install -e . && pytest" environment: - DOCKER_TEST=TRUE volumes: diff --git a/jellyfish/__init__.py b/jellyfish/__init__.py index 71ddcd7..461227d 100644 --- a/jellyfish/__init__.py +++ b/jellyfish/__init__.py @@ -1,15 +1,21 @@ """ - Python server SDK for [Jellyfish](https://github.com/jellyfish-dev/jellyfish) media server. + .. include:: ../README.md """ # pylint: disable=locally-disabled, no-name-in-module, import-error from pydantic.error_wrappers import ValidationError -from jellyfish._openapi_client import Room, RoomConfig, Peer, Component -from jellyfish._openapi_client import ComponentOptions, ComponentOptionsRTSP, PeerOptionsWebRTC +from jellyfish._openapi_client import ( + Room, RoomConfig, Peer, Component, ComponentOptions, ComponentOptionsRTSP, PeerOptionsWebRTC) from jellyfish._openapi_client.exceptions import ( UnauthorizedException, NotFoundException, BadRequestException) from jellyfish._room_api import RoomApi + +__all__ = ['RoomApi', 'Room', 'Peer', 'Component', 'RoomConfig', 'ComponentOptions', + 'ComponentOptionsRTSP', 'PeerOptionsWebRTC', 'UnauthorizedException', + 'NotFoundException', 'BadRequestException'] + +__docformat__ = "restructuredtext" diff --git a/jellyfish/_room_api.py b/jellyfish/_room_api.py index 4a49b68..5f89c25 100644 --- a/jellyfish/_room_api.py +++ b/jellyfish/_room_api.py @@ -5,13 +5,17 @@ from jellyfish import _openapi_client as jellyfish_api from jellyfish._openapi_client import (AddPeerRequest, AddComponentRequest, PeerOptions, - ComponentOptions, Room, RoomConfig, Peer, Component) + ComponentOptions, Room, RoomConfig, Peer, Component) class RoomApi: - """Allows for managing rooms""" + '''Allows for managing rooms''' - def __init__(self, server_address: str, server_api_token: str): + def __init__(self, + server_address: str = 'localhost:5002', server_api_token: str = 'development'): + ''' + Create RoomApi instance, providing the jellyfish address and api token. + ''' self._configuration = jellyfish_api.Configuration( host=server_address, access_token=server_api_token @@ -21,30 +25,41 @@ def __init__(self, server_address: str, server_api_token: str): self._room_api = jellyfish_api.RoomApi(self._api_client) def create_room(self, max_peers: int = None, video_codec: str = None) -> (str, Room): - """Creates a room""" + ''' + Creates a new room + Returns a tuple (`jellyfish_address`, `Room`) - the address of the Jellyfish + in which the room has been created and the created `Room` + ''' room_config = RoomConfig(maxPeers=max_peers, videoCodec=video_codec) resp = self._room_api.create_room(room_config) return (resp.data.jellyfish_address, resp.data.room) def delete_room(self, room_id: str) -> None: - """Deletes a room""" + '''Deletes a room''' return self._room_api.delete_room(room_id) def get_all_rooms(self) -> list: - """Returns list of all rooms""" + '''Returns list of all rooms ''' return self._room_api.get_all_rooms().data def get_room(self, room_id: str) -> Room: - """Returns room with the given id""" + '''Returns room with the given id''' return self._room_api.get_room(room_id).data def add_peer(self, room_id: str, peer_type: str, options) -> (str, Peer): - """Creates peer in the room""" + ''' + Creates peer in the room + + Currently only `'webrtc'` peer is supported + + Returns a tuple (`peer_token`, `Peer`) - the token needed by Peer to authenticate + to Jellyfish and the new `Peer` + ''' options = PeerOptions(options) request = AddPeerRequest(type=peer_type, options=options) @@ -53,12 +68,12 @@ def add_peer(self, room_id: str, peer_type: str, options) -> (str, Peer): return (resp.data.token, resp.data.peer) def delete_peer(self, room_id: str, peer_id: str) -> None: - """Deletes peer""" + '''Deletes peer''' return self._room_api.delete_peer(room_id, peer_id) def add_component(self, room_id: str, component_type: str, options=None) -> Component: - """Creates component in the room""" + '''Creates component in the room''' if options is not None: options = ComponentOptions(options) @@ -68,6 +83,6 @@ def add_component(self, room_id: str, component_type: str, options=None) -> Comp return self._room_api.add_component(room_id, request).data def delete_component(self, room_id: str, component_id: str) -> None: - """Deletes component""" + '''Deletes component''' return self._room_api.delete_component(room_id, component_id) diff --git a/pdoc.sh b/pdoc.sh new file mode 100755 index 0000000..5040571 --- /dev/null +++ b/pdoc.sh @@ -0,0 +1,6 @@ +pdoc\ + --favicon https://logo.swmansion.com/membrane/\?width\=100\&variant\=signetDark\ + --logo https://logo.swmansion.com/membrane/\?width\=70\&variant\=signetDark\ + -t doc_templates \ + -o doc \ + jellyfish diff --git a/pylint.sh b/pylint.sh index 156aaca..84cad9e 100755 --- a/pylint.sh +++ b/pylint.sh @@ -1,3 +1,3 @@ #!/bin/bash -pylint --rcfile=pylintrc --ignore-paths jellyfish/_openapi_client/ jellyfish tests +pylint --rcfile=pylintrc jellyfish tests diff --git a/pylintrc b/pylintrc index 9078b89..594df76 100644 --- a/pylintrc +++ b/pylintrc @@ -52,7 +52,7 @@ ignore=CVS # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths= +ignore-paths=jellyfish/_openapi_client/ # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores diff --git a/pyproject.toml b/pyproject.toml index c4ff946..808f076 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.0.6" authors = [ { name="Membrane Team" }, ] -description = "Python server SDK for Jellyfish media server" +description = "Python server SDK for the Jellyfish media server" readme = "README.md" requires-python = ">=3.8" classifiers = [ diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 0f62ab7..59698b3 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -34,13 +34,13 @@ class TestAuthentication: - def test_invalid_token(self, room_api): + def test_invalid_token(self): room_api = RoomApi(server_address=SERVER_ADDRESS, server_api_token="invalid") with pytest.raises(UnauthorizedException): room_api.create_room() - def test_valid_token(self, room_api): + def test_valid_token(self): room_api = RoomApi(server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN) _, room = room_api.create_room() @@ -49,6 +49,15 @@ def test_valid_token(self, room_api): assert room in all_rooms + def test_default_api_token(self): + room_api = RoomApi(server_address=SERVER_ADDRESS) + + _, room = room_api.create_room() + + all_rooms = room_api.get_all_rooms() + + assert room in all_rooms + @pytest.fixture def room_api():