diff --git a/.ruff.toml b/.ruff.toml index a6e15d897..5219a2b40 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -4,7 +4,6 @@ lint.unfixable = [] lint.ignore = [ "ANN003", - "ANN101", "ARG001", "ARG002", "ARG005", diff --git a/doc/conf.py b/doc/conf.py index 791a9d2e6..bf66e7330 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,200 +1,200 @@ -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -# ruff: noqa: A001 - -# -- 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 ----------------------------------------------------- - -project = 'IPv8' -copyright = '2017-2024, Tribler' # Do not change manually! Handled by github_increment_version.py -author = 'Tribler' - -# The short X.Y version -version = '2.14' # Do not change manually! Handled by github_increment_version.py -# The full version, including alpha/beta/rc tags -release = '2.14.0' # Do not change manually! Handled by github_increment_version.py - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'autoapi.extension', - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', -] - -autoapi_type = 'python' -autoapi_add_toctree_entry = False -autoapi_dirs = ['../ipv8'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = None - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'IPv8doc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'IPv8.tex', 'IPv8 Documentation', - 'Tribler', 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'ipv8', 'IPv8 Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'IPv8', 'IPv8 Documentation', - author, 'IPv8', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - - -# -- Extension configuration ------------------------------------------------- - -# -- Options for intersphinx extension --------------------------------------- - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} - -# -- Options for todo extension ---------------------------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# ruff: noqa: A001 + +# -- 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 ----------------------------------------------------- + +project = 'IPv8' +copyright = '2017-2024, Tribler' # Do not change manually! Handled by github_increment_version.py +author = 'Tribler' + +# The short X.Y version +version = '3.0' # Do not change manually! Handled by github_increment_version.py +# The full version, including alpha/beta/rc tags +release = '3.0.0' # Do not change manually! Handled by github_increment_version.py + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'autoapi.extension', + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +autoapi_type = 'python' +autoapi_add_toctree_entry = False +autoapi_dirs = ['../ipv8'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'IPv8doc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'IPv8.tex', 'IPv8 Documentation', + 'Tribler', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'ipv8', 'IPv8 Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'IPv8', 'IPv8 Documentation', + author, 'IPv8', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/github_increment_version.py b/github_increment_version.py index e086a7e8b..ff1957838 100644 --- a/github_increment_version.py +++ b/github_increment_version.py @@ -22,7 +22,7 @@ from packaging.version import Version -# ruff: noqa: S603,S607,T201 +# ruff: noqa: S602,S603,S607,T201 def parse_setup() -> tuple[str, ast.Expr]: @@ -220,36 +220,36 @@ def modify_rest_manager(file_contents: str, element: ast.Constant, new_version_t print(f"[4/8] Retrieving Tribler:py-ipv8 and {username}:py-ipv8.") # branchname or "HEAD" -original_branch = subprocess.check_output("git rev-parse --abbrev-ref HEAD", encoding="utf-8").strip() +original_branch = subprocess.check_output("git rev-parse --abbrev-ref HEAD", encoding="utf-8", shell=True).strip() if original_branch == "HEAD": # HEAD, origin/main, origin/HEAD - detached_details = subprocess.check_output("git show -s --pretty=%D HEAD", encoding="utf-8") + detached_details = subprocess.check_output("git show -s --pretty=%D HEAD", encoding="utf-8", shell=True) original_branch = detached_details.split(", ")[1].strip() -print(subprocess.check_output(f"git remote add __{username} git@github.com:{username}/py-ipv8.git", encoding="utf-8")) -print(subprocess.check_output("git remote add __Tribler git@github.com:Tribler/py-ipv8.git", encoding="utf-8")) +print(subprocess.check_output(f"git remote add __{username} git@github.com:{username}/py-ipv8.git", encoding="utf-8", shell=True)) +print(subprocess.check_output("git remote add __Tribler git@github.com:Tribler/py-ipv8.git", encoding="utf-8", shell=True)) -print(subprocess.check_output("git fetch __Tribler master", encoding="utf-8")) -print(subprocess.check_output("git checkout -b __automated_version_update __Tribler/master", encoding="utf-8")) +print(subprocess.check_output("git fetch __Tribler master", encoding="utf-8", shell=True)) +print(subprocess.check_output("git checkout -b __automated_version_update __Tribler/master", encoding="utf-8", shell=True)) # GET CHANGES print("[5/8] Calculating changes since last release.") -known_tags = sorted(Version(t) for t in subprocess.check_output("git tag -l", encoding="utf-8").split()) -last_release_commit, = subprocess.check_output(f"git rev-list -n 1 {known_tags[-1]}", encoding="utf-8").split() +known_tags = sorted(Version(t) for t in subprocess.check_output("git tag -l", encoding="utf-8", shell=True).split()) +last_release_commit, = subprocess.check_output(f"git rev-list -n 1 {known_tags[-1]}", encoding="utf-8", shell=True).split() git_log = subprocess.check_output(f'git log {last_release_commit}..HEAD --pretty=format:"%H"', - encoding="utf-8").split("\n") + encoding="utf-8", shell=True).split("\n") git_log = [ ( - subprocess.check_output(f"git log --format=%B -n 1 {sha_entry}", encoding="utf-8").split("\n")[0], - subprocess.check_output(f"git log --format=%b -n 1 {sha_entry}", encoding="utf-8").split("\n")[0] + subprocess.check_output(f"git log --format=%B -n 1 {sha_entry}", encoding="utf-8", shell=True).split("\n")[0], + subprocess.check_output(f"git log --format=%b -n 1 {sha_entry}", encoding="utf-8", shell=True).split("\n")[0] ) for sha_entry in git_log ] commits_since_last = len(git_log) + 2 -total_commits_str = subprocess.check_output("git rev-list --count HEAD", encoding="utf-8") +total_commits_str = subprocess.check_output("git rev-list --count HEAD", encoding="utf-8", shell=True) total_commits = int(total_commits_str) + 2 # PERFORM FILE REWRITES @@ -265,18 +265,18 @@ def modify_rest_manager(file_contents: str, element: ast.Constant, new_version_t # CREATE FEATURE BRANCH print("[7/8] Pushing changes to branch on fork.") -print(subprocess.check_output("git add setup.py", encoding="utf-8")) -print(subprocess.check_output("git add doc/conf.py", encoding="utf-8")) -print(subprocess.check_output("git add ipv8/REST/rest_manager.py", encoding="utf-8")) -print(subprocess.check_output('git commit -m "Automated version increment"', encoding="utf-8")) +print(subprocess.check_output("git add setup.py", encoding="utf-8", shell=True)) +print(subprocess.check_output("git add doc/conf.py", encoding="utf-8", shell=True)) +print(subprocess.check_output("git add ipv8/REST/rest_manager.py", encoding="utf-8", shell=True)) +print(subprocess.check_output('git commit -m "Automated version increment"', encoding="utf-8", shell=True)) print(subprocess.check_output(f"git push -f -u __{username} __automated_version_update:automated_version_update", - encoding="utf-8")) + encoding="utf-8", shell=True)) # > Cleanup -print(subprocess.check_output(f"git checkout {original_branch}", encoding="utf-8")) -print(subprocess.check_output("git branch -D __automated_version_update", encoding="utf-8")) -print(subprocess.check_output("git remote remove __Tribler", encoding="utf-8")) -print(subprocess.check_output(f"git remote remove __{username}", encoding="utf-8")) +print(subprocess.check_output(f"git checkout {original_branch}", encoding="utf-8", shell=True)) +print(subprocess.check_output("git branch -D __automated_version_update", encoding="utf-8", shell=True)) +print(subprocess.check_output("git remote remove __Tribler", encoding="utf-8", shell=True)) +print(subprocess.check_output(f"git remote remove __{username}", encoding="utf-8", shell=True)) # CREATE PULL REQUEST print("[8/8] Formatting Pull Request message") @@ -329,15 +329,15 @@ def commit_messages_to_names(commit_msg_list: list[str]) -> list[str]: for commit_msg in commit_msg_list: corrected = commit_msg # First, strip residual prefixes, e.g. "READY: Add some feature" -> "Add some feature". - for mistake in residual_prefixes: + for mistake, correction in residual_prefixes.items(): if commit_msg.startswith(mistake): - corrected = residual_prefixes[mistake] + commit_msg[len(mistake):] + corrected = correction + commit_msg[len(mistake):] # Second, modify misspellings, e.g. "Add some feature" -> "Added some feature". # We do this after the first step to correct compound errors (both leaving the prefix AND not adhering to # the naming standard). - for mistake in misspellings: + for mistake, correction in misspellings.items(): if corrected.startswith(mistake): - corrected = misspellings[mistake] + corrected[len(mistake):] + corrected = correction + corrected[len(mistake):] out.append(corrected) return sorted(out) diff --git a/ipv8/REST/rest_manager.py b/ipv8/REST/rest_manager.py index 07fcfecf0..7facd93e7 100644 --- a/ipv8/REST/rest_manager.py +++ b/ipv8/REST/rest_manager.py @@ -1,153 +1,153 @@ -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, cast - -from aiohttp import web -from aiohttp_apispec import AiohttpApiSpec - -from .base_endpoint import HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, BaseEndpoint, Response -from .root_endpoint import RootEndpoint - -if TYPE_CHECKING: - from aiohttp.abc import Request - from aiohttp.connector import SSLContext - from aiohttp.typedefs import Handler - from aiohttp.web_response import StreamResponse - from aiohttp.web_runner import BaseRunner - - -@web.middleware -class ApiKeyMiddleware: - """ - Middleware to check for authorized REST access. - """ - - def __init__(self, api_key: str | None) -> None: - """ - Create new middleware for the given API key. - """ - self.api_key = api_key - - async def __call__(self, request: Request, handler: Handler) -> StreamResponse | Response: - """ - Intercept requests that are not authorized. - """ - if self.authenticate(request): - return await handler(request) - return Response({'error': 'Unauthorized access'}, status=HTTP_UNAUTHORIZED) - - def authenticate(self, request: Request) -> bool: - """ - Check if the given request is authorized. - """ - if request.path.startswith('/docs') or request.path.startswith('/static'): - return True - # The api key can either be in the headers or as part of the url query - api_key = request.headers.get('X-Api-Key') or request.query.get('apikey') - return not self.api_key or self.api_key == api_key - - -@web.middleware -async def cors_middleware(request: Request, handler: Handler) -> Response | StreamResponse: - """ - Cross-origin resource sharing middleware. - """ - preflight_cors = request.method == "OPTIONS" and 'Access-Control-Request-Method' in request.headers - if not preflight_cors: - return await handler(request) - - response = web.StreamResponse() - # For now, just allow all methods - response.headers['Access-Control-Allow-Methods'] = "GET, PUT, POST, PATCH, DELETE, OPTIONS" - response.headers['Access-Control-Allow-Headers'] = '*' - response.headers['Access-Control-Allow-Origin'] = '*' - response.headers['Access-Control-Max-Age'] = str(86400) - return response - - -@web.middleware -async def error_middleware(request: Request, handler: Handler) -> Response | StreamResponse: - """ - Middleware to catch call errors when handling requests. - """ - try: - response = await handler(request) - except Exception as e: - import traceback - traceback.print_exc() - return Response({ - "success": False, - "error": { - "code": e.__class__.__name__, - "message": str(e) - } - }, status=HTTP_INTERNAL_SERVER_ERROR) - return response - - -class RESTManager: - """ - This class is responsible for managing the startup and closing of the HTTP API. - """ - - def __init__(self, session: object, root_endpoint_class: type[BaseEndpoint] | None = None) -> None: - """ - Create a new manager to orchestrate REST requests and responses. - """ - self._logger = logging.getLogger(self.__class__.__name__) - self.session = session - self.site: web.TCPSite | None = None - self.root_endpoint: BaseEndpoint | None = None - self._root_endpoint_class = root_endpoint_class or RootEndpoint - - async def start(self, port: int = 8085, host: str = '127.0.0.1', api_key: str | None = None, - ssl_context: SSLContext | None = None) -> None: - """ - Starts the HTTP API with the listen port as specified in the session configuration. - """ - self.root_endpoint = self._root_endpoint_class(middlewares=[ApiKeyMiddleware(api_key), - cors_middleware, - error_middleware]) - self.root_endpoint.initialize(self.session) - - # Not using setup_aiohttp_apispec here, as we need access to the APISpec to set the security scheme - aiohttp_apispec = AiohttpApiSpec( - app=self.root_endpoint.app, - title="IPv8 REST API documentation", - version="v2.14", # Do not change manually! Handled by github_increment_version.py - url="/docs/swagger.json", - swagger_path="/docs", - ) - if api_key: - # Set security scheme and apply to all endpoints - aiohttp_apispec.spec.options['security'] = [{'apiKey': []}] - aiohttp_apispec.spec.components.security_scheme('apiKey', {'type': 'apiKey', - 'in': 'header', - 'name': 'X-Api-Key'}) - - from apispec.core import VALID_METHODS_OPENAPI_V2 - if 'head' in VALID_METHODS_OPENAPI_V2: - VALID_METHODS_OPENAPI_V2.remove('head') - - runner = web.AppRunner(self.root_endpoint.app, access_log=None) - await runner.setup() - await self.start_site(runner, host, port, ssl_context) - - async def start_site(self, runner: BaseRunner, host: str | None, port: int | None, - ssl_context: SSLContext | None) -> None: - """ - Create and start the internal TCP-based site. - """ - # If localhost is used as hostname, it will randomly either use 127.0.0.1 or ::1 - self.site = web.TCPSite(runner, host, port, ssl_context=ssl_context) - await self.site.start() - - async def stop(self) -> None: - """ - Stop the HTTP API and return when the server has shut down. - """ - if self.site is None: - return - self.site = cast(web.TCPSite, self.site) - await self.site.stop() +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +from aiohttp import web +from aiohttp_apispec import AiohttpApiSpec + +from .base_endpoint import HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, BaseEndpoint, Response +from .root_endpoint import RootEndpoint + +if TYPE_CHECKING: + from aiohttp.abc import Request + from aiohttp.connector import SSLContext + from aiohttp.typedefs import Handler + from aiohttp.web_response import StreamResponse + from aiohttp.web_runner import BaseRunner + + +@web.middleware +class ApiKeyMiddleware: + """ + Middleware to check for authorized REST access. + """ + + def __init__(self, api_key: str | None) -> None: + """ + Create new middleware for the given API key. + """ + self.api_key = api_key + + async def __call__(self, request: Request, handler: Handler) -> StreamResponse | Response: + """ + Intercept requests that are not authorized. + """ + if self.authenticate(request): + return await handler(request) + return Response({'error': 'Unauthorized access'}, status=HTTP_UNAUTHORIZED) + + def authenticate(self, request: Request) -> bool: + """ + Check if the given request is authorized. + """ + if request.path.startswith('/docs') or request.path.startswith('/static'): + return True + # The api key can either be in the headers or as part of the url query + api_key = request.headers.get('X-Api-Key') or request.query.get('apikey') + return not self.api_key or self.api_key == api_key + + +@web.middleware +async def cors_middleware(request: Request, handler: Handler) -> Response | StreamResponse: + """ + Cross-origin resource sharing middleware. + """ + preflight_cors = request.method == "OPTIONS" and 'Access-Control-Request-Method' in request.headers + if not preflight_cors: + return await handler(request) + + response = web.StreamResponse() + # For now, just allow all methods + response.headers['Access-Control-Allow-Methods'] = "GET, PUT, POST, PATCH, DELETE, OPTIONS" + response.headers['Access-Control-Allow-Headers'] = '*' + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Max-Age'] = str(86400) + return response + + +@web.middleware +async def error_middleware(request: Request, handler: Handler) -> Response | StreamResponse: + """ + Middleware to catch call errors when handling requests. + """ + try: + response = await handler(request) + except Exception as e: + import traceback + traceback.print_exc() + return Response({ + "success": False, + "error": { + "code": e.__class__.__name__, + "message": str(e) + } + }, status=HTTP_INTERNAL_SERVER_ERROR) + return response + + +class RESTManager: + """ + This class is responsible for managing the startup and closing of the HTTP API. + """ + + def __init__(self, session: object, root_endpoint_class: type[BaseEndpoint] | None = None) -> None: + """ + Create a new manager to orchestrate REST requests and responses. + """ + self._logger = logging.getLogger(self.__class__.__name__) + self.session = session + self.site: web.TCPSite | None = None + self.root_endpoint: BaseEndpoint | None = None + self._root_endpoint_class = root_endpoint_class or RootEndpoint + + async def start(self, port: int = 8085, host: str = '127.0.0.1', api_key: str | None = None, + ssl_context: SSLContext | None = None) -> None: + """ + Starts the HTTP API with the listen port as specified in the session configuration. + """ + self.root_endpoint = self._root_endpoint_class(middlewares=[ApiKeyMiddleware(api_key), + cors_middleware, + error_middleware]) + self.root_endpoint.initialize(self.session) + + # Not using setup_aiohttp_apispec here, as we need access to the APISpec to set the security scheme + aiohttp_apispec = AiohttpApiSpec( + app=self.root_endpoint.app, + title="IPv8 REST API documentation", + version="v3.0", # Do not change manually! Handled by github_increment_version.py + url="/docs/swagger.json", + swagger_path="/docs", + ) + if api_key: + # Set security scheme and apply to all endpoints + aiohttp_apispec.spec.options['security'] = [{'apiKey': []}] + aiohttp_apispec.spec.components.security_scheme('apiKey', {'type': 'apiKey', + 'in': 'header', + 'name': 'X-Api-Key'}) + + from apispec.core import VALID_METHODS_OPENAPI_V2 + if 'head' in VALID_METHODS_OPENAPI_V2: + VALID_METHODS_OPENAPI_V2.remove('head') + + runner = web.AppRunner(self.root_endpoint.app, access_log=None) + await runner.setup() + await self.start_site(runner, host, port, ssl_context) + + async def start_site(self, runner: BaseRunner, host: str | None, port: int | None, + ssl_context: SSLContext | None) -> None: + """ + Create and start the internal TCP-based site. + """ + # If localhost is used as hostname, it will randomly either use 127.0.0.1 or ::1 + self.site = web.TCPSite(runner, host, port, ssl_context=ssl_context) + await self.site.start() + + async def stop(self) -> None: + """ + Stop the HTTP API and return when the server has shut down. + """ + if self.site is None: + return + self.site = cast(web.TCPSite, self.site) + await self.site.stop() diff --git a/setup.py b/setup.py index 459d53179..4a660ab0f 100644 --- a/setup.py +++ b/setup.py @@ -1,48 +1,48 @@ -from setuptools import find_packages, setup - -with open("README.md") as fh: - long_description = fh.read() - -setup( - name='pyipv8', - author='Tribler', - description='The Python implementation of the IPV8 library', - long_description=long_description, - long_description_content_type='text/markdown', - version='2.14.0', # Do not change manually! Handled by github_increment_version.py - url='https://github.com/Tribler/py-ipv8', - package_data={'': ['*.*']}, - packages=find_packages(), - py_modules=['ipv8_service'], - install_requires=[ - "cryptography", - "libnacl", - "aiohttp", - "aiohttp_apispec", - "pyOpenSSL", - "pyasn1", - "marshmallow", - "typing-extensions", - "packaging" - ], - extras_require={ - "all": ["coverage"], - "tests": ["coverage"] - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Scientific/Engineering", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: System :: Distributed Computing", - "Topic :: System :: Networking" - ] -) +from setuptools import find_packages, setup + +with open("README.md") as fh: + long_description = fh.read() + +setup( + name='pyipv8', + author='Tribler', + description='The Python implementation of the IPV8 library', + long_description=long_description, + long_description_content_type='text/markdown', + version='3.0.0', # Do not change manually! Handled by github_increment_version.py + url='https://github.com/Tribler/py-ipv8', + package_data={'': ['*.*']}, + packages=find_packages(), + py_modules=['ipv8_service'], + install_requires=[ + "cryptography", + "libnacl", + "aiohttp", + "aiohttp_apispec", + "pyOpenSSL", + "pyasn1", + "marshmallow", + "typing-extensions", + "packaging" + ], + extras_require={ + "all": ["coverage"], + "tests": ["coverage"] + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Distributed Computing", + "Topic :: System :: Networking" + ] +)