From 8aa97ea3cd20d9ba91aea157bc98e793747fe075 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Mon, 30 Dec 2024 00:30:02 +0000 Subject: [PATCH 1/5] chore: deprecate views (#1096) * chore: deprecate views * update docs * complete * fix issue * update * update * update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../src/components/documentation/ApiDocs.jsx | 8 - .../components/documentation/Navigation.jsx | 27 ++- .../documentation/api_reference/views.mdx | 182 ------------------ .../example_app/subrouters_and_views.mdx | 105 +++------- integration_tests/base_routes.py | 26 --- integration_tests/test_views.py | 51 ----- integration_tests/views/__init__.py | 4 - integration_tests/views/async_view.py | 9 - integration_tests/views/sync_view.py | 9 - poetry.lock | 28 +-- pyproject.toml | 1 - robyn/__init__.py | 46 +---- 12 files changed, 54 insertions(+), 442 deletions(-) delete mode 100644 docs_src/src/pages/documentation/api_reference/views.mdx delete mode 100644 integration_tests/test_views.py delete mode 100644 integration_tests/views/__init__.py delete mode 100644 integration_tests/views/async_view.py delete mode 100644 integration_tests/views/sync_view.py diff --git a/docs_src/src/components/documentation/ApiDocs.jsx b/docs_src/src/components/documentation/ApiDocs.jsx index 652ca16f3..01b1457c1 100644 --- a/docs_src/src/components/documentation/ApiDocs.jsx +++ b/docs_src/src/components/documentation/ApiDocs.jsx @@ -68,12 +68,6 @@ const guides = [ name: 'Websockets', description: 'Learn how to use Websockets in Robyn.', }, - { - href: '/documentation/api_reference/views', - name: 'Code Organisation', - description: 'Learn about Views and SubRouters in Robyn.', - }, - { href: '/documentation/api_reference/exceptions', name: 'Exceptions', @@ -94,8 +88,6 @@ const guides = [ name: 'Multiprocess Execution', description: 'Learn about the behaviour or variables during multithreading', }, - - { href: '/documentation/api_reference/using_rust_directly', name: 'Direct Rust Usage', diff --git a/docs_src/src/components/documentation/Navigation.jsx b/docs_src/src/components/documentation/Navigation.jsx index 2ad4b7225..2169baeee 100644 --- a/docs_src/src/components/documentation/Navigation.jsx +++ b/docs_src/src/components/documentation/Navigation.jsx @@ -216,8 +216,8 @@ export const navigation = [ }, { title: 'Templates', href: '/documentation/example_app/templates' }, { - title: 'SubRouters and Views', - href: '/documentation/example_app/subrouters_and_views', + title: 'SubRouters', + href: '/documentation/example_app/subrouters', }, ], }, @@ -276,15 +276,6 @@ export const navigation = [ href: '/documentation/api_reference/websockets', title: 'Websockets', }, - { - href: '/documentation/api_reference/views', - title: 'Code Organisation', - }, - { - href: '/documentation/api_reference/dependency_injection', - title: 'Dependency Injection', - }, - { href: '/documentation/api_reference/exceptions', title: 'Exceptions', @@ -297,22 +288,26 @@ export const navigation = [ href: '/documentation/api_reference/advanced_features', title: 'Advanced Features', }, - { - title: 'OpenAPI Documentation', - href: '/documentation/api_reference/openapi', - }, { href: '/documentation/api_reference/multiprocess_execution', title: 'Multiprocess Execution', }, { href: '/documentation/api_reference/using_rust_directly', - title: 'Using Rust Directly', + title: 'Direct Rust Usage', }, { href: '/documentation/api_reference/graphql-support', title: 'GraphQL Support', }, + { + href: '/documentation/api_reference/openapi', + title: 'OpenAPI Documentation', + }, + { + href: '/documentation/api_reference/dependency_injection', + title: 'Dependency Injection', + } ], }, { diff --git a/docs_src/src/pages/documentation/api_reference/views.mdx b/docs_src/src/pages/documentation/api_reference/views.mdx deleted file mode 100644 index fa3920e42..000000000 --- a/docs_src/src/pages/documentation/api_reference/views.mdx +++ /dev/null @@ -1,182 +0,0 @@ -export const description = - 'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.' - - -As the codebase grew, Batman wanted to have a way to organise the code. Robyn told him that he had the super power to organise the code in a better way. He told him about the concept of views and subrouters. - -## Views - - - - - -To organise your code in a better way - either to group by responsibility or for code splitting, you can use views. - -A view, simply is a function with a collection of other closures. e.g. - - - - - - ```python {{ title: 'untyped' }} - def sample_view(): - def get(): - return "Hello, world!" - - def post(request): - body = request.body - return Response({"status_code": 200, "description": body, "headers": {}}) - - ``` - - ```python {{title: 'typed'}} - from robyn import Request - - def sample_view(): - def get(): - return "Hello, world!" - - def post(request: Request): - body = request.body - return Response({"status_code": 200, "description": body, "headers": {}}) - ``` - - - -The above view contains two closures for the get and the post request. - -You can serve views in two ways: - - - - Using an `@app.view` decorator. - - - - - ```python {{ title: 'untyped' }} - @app.view("/sync/view/decorator") - def sync_decorator_view(): - def get(): - return "Hello, world!" - - def post(request): - body = request.body - return body - - ``` - ```python {{ title: 'typed' }} - from robyn import Request - - @app.view("/sync/view/decorator") - def sync_decorator_view(): - def get(): - return "Hello, world!" - - def post(request: Request): - body = request.body - return body - - ``` - - - - - - - Using the `app.add_view` method. - - - - - ```python {{ title: 'untyped' }} - from .views import sample_view - - ... - ... - - app.add_view("/", sample_view) - ``` - ```python {{ title: 'typed' }} - from .views import sample_view - - ... - ... - - app.add_view("/", sample_view) - ``` - - - - - ---- - -## Subrouters - - - - - - - - -Batman can create subrouters in Robyn. This is useful when you want to group routes together. - -Subrouters can be used for both normal routes and web sockets. They are basically a mini version of the main router and can be used in the same way. - -The only thing to remember is that you need to add the subrouter to the main router. - - - - - ```python {{ title: 'untyped' }} - from robyn import Robyn, SubRouter - - app = Robyn(__file__) - - sub_router = SubRouter("/sub_router") - - @sub_router.get("/hello") - def hello(): - return "Hello, world" - - web_socket = SubRouter("/web_socket") - - @web_socket.message() - async def hello(): - return "Hello, world" - - app.include_router(sub_router) - ``` - - - ```python {{ title: 'typed' }} - from robyn import Robyn, SubRouter - - app = Robyn(__file__) - - sub_router = SubRouter("/sub_router") - - @sub_router.get("/hello") - def hello(): - return "Hello, world" - - web_socket = SubRouter("/web_socket") - - @web_socket.message() - async def hello(): - return "Hello, world" - - app.include_router(sub_router) - ``` - - - - -## What's next? - -Now, that Batman knows how to organise his code, he wants to know how to add dependencies to his code. Robyn tells him that he can add dependencies at the global level, router level and the view level. - -- [Dependency Injection](/documentation/api_reference/dependency_injection) - diff --git a/docs_src/src/pages/documentation/example_app/subrouters_and_views.mdx b/docs_src/src/pages/documentation/example_app/subrouters_and_views.mdx index d2bc2824b..12772a7a0 100644 --- a/docs_src/src/pages/documentation/example_app/subrouters_and_views.mdx +++ b/docs_src/src/pages/documentation/example_app/subrouters_and_views.mdx @@ -1,97 +1,48 @@ export const description = 'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.' +## Code Organization with SubRouters -## SubRouter and Views +As the application grew, Batman needed a way to organize his routes better. He decided to use Robyn's SubRouter feature to group related routes together. -After implementing the application application, Batman wanted to split the codebase into multiple files. - -This is when Robyn introduced him to the concept of routers and views. - -### Routers - -Routers are a way to split your application into multiple files. They allow you to group related endpoints together and make it easier to maintain your codebase. - -For example, if you wanted to create a router for the frontend, you would create a file called `frontend.py`. This file would contain all the endpoints related to the frontend. - -So the folder structure would look like this: - -```bash -├── app.py -├── frontend.py -├── Dockerfile -└── requirements.txt -``` - -And the code would look like this: - -```python {{ title: 'Creating a Router' }} -# frontend.py - -from robyn.templating import JinjaTemplate +```python from robyn import SubRouter -import os -import pathlib - -current_file_path = pathlib.Path(__file__).parent.resolve() -jinja_template = JinjaTemplate(os.path.join(current_file_path, "templates")) +# Create a subrouter for crime-related routes +crime_router = SubRouter(__file__, prefix="/crimes") +@crime_router.get("/list") +def list_crimes(): + return {"crimes": get_all_crimes()} -frontend = SubRouter(__name__, prefix="/frontend") +@crime_router.post("/report") +def report_crime(request): + crime_data = request.json() + return {"id": create_crime_report(crime_data)} -@frontend.get("/") -async def get_frontend(request): - context = {"framework": "Robyn", "templating_engine": "Jinja2"} - return jinja_template.render_template("index.html", **context) -``` +# Create a subrouter for suspect-related routes +suspect_router = SubRouter(__file__, prefix="/suspects") -```python {{ title: 'Including a Router' }} -# app.py +@suspect_router.get("/list") +def list_suspects(): + return {"suspects": get_all_suspects()} -from .frontend import frontend +@suspect_router.get("/:id") +def get_suspect(request, path_params): + suspect_id = path_params.id + return {"suspect": get_suspect_by_id(suspect_id)} - -app.include_router(frontend) +# Include the subrouters in the main app +app.include_router(crime_router) +app.include_router(suspect_router) ``` +SubRouters help organize related routes under a common prefix, making the code more maintainable and easier to understand. In this example: -### Views - -Views are a way to split your application into multiple files. They allow you to group related endpoints together and make it easier to maintain your codebase. - -For example, if you wanted to create a view for the frontend, you would create a file called `frontend.py`. This file would contain all the endpoints related to the frontend. - - -The code would look like this: - - - ```python {{ title: 'Creating a decorator View' }} - from robyn import SyncView - - @app.view("/sync/view/decorator") - def sync_decorator_view(): - def get(): - return "Hello, world!" - - def post(request: Request): - body = request.body - return body - ``` - - - ```python {{ title: 'Creating a View' }} - def sync_decorator_view(): - def get(): - return "Hello, world!" - - def post(request: Request): - body = request.body - return body +- All crime-related routes are under `/crimes` +- All suspect-related routes are under `/suspects` - app.add_view("/sync/view/decorator", sync_decorator_view) - ``` - +This organization makes it clear which routes handle what functionality and keeps related code together. diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 54e2e475e..5092ae9a1 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -4,7 +4,6 @@ from typing import Optional from integration_tests.subroutes import di_subrouter, sub_router -from integration_tests.views import AsyncView, SyncView from robyn import Headers, Request, Response, Robyn, WebSocket, WebSocketConnector, jsonify, serve_file, serve_html from robyn.authentication import AuthenticationHandler, BearerGetter, Identity from robyn.robyn import QueryParams, Url @@ -712,29 +711,6 @@ async def async_body_patch(request: Request): return request.body -# ===== Views ===== - - -@app.view("/sync/view/decorator") -def sync_decorator_view(): - def get(): - return "Hello, world!" - - def post(request: Request): - body = request.body - return body - - -@app.view("/async/view/decorator") -def async_decorator_view(): - async def get(): - return "Hello, world!" - - async def post(request: Request): - body = request.body - return body - - # ==== Exception Handling ==== @@ -1090,8 +1066,6 @@ def main(): index_file="index.html", ) app.startup_handler(startup_handler) - app.add_view("/sync/view", SyncView) - app.add_view("/async/view", AsyncView) app.include_router(sub_router) app.include_router(di_subrouter) diff --git a/integration_tests/test_views.py b/integration_tests/test_views.py deleted file mode 100644 index 76d97d1e8..000000000 --- a/integration_tests/test_views.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest - -from integration_tests.helpers.http_methods_helpers import get, post - - -@pytest.mark.benchmark -def test_get_sync_view(session): - r = get("/sync/view") - assert r.text == "Hello, world!" - - -@pytest.mark.benchmark -def test_post_sync_view(session): - r = post("/sync/view", data={"name": "John"}) - assert "John" in r.text - - -@pytest.mark.benchmark -def test_get_sync_decorator_view(session): - r = get("/sync/view/decorator") - assert r.text == "Hello, world!" - - -@pytest.mark.benchmark -def test_post_sync_decorator_view(session): - r = post("/sync/view/decorator", data={"name": "John"}) - assert "John" in r.text - - -@pytest.mark.benchmark -def test_get_async_view(session): - r = get("/async/view") - assert r.text == "Hello, world!" - - -@pytest.mark.benchmark -def test_post_async_view(session): - r = post("/async/view", data={"name": "John"}) - assert "John" in r.text - - -@pytest.mark.benchmark -def test_get_async_decorator_view(session): - r = get("/async/view/decorator") - assert r.text == "Hello, world!" - - -@pytest.mark.benchmark -def test_post_async_decorator_view(session): - r = post("/async/view/decorator", data={"name": "John"}) - assert "John" in r.text diff --git a/integration_tests/views/__init__.py b/integration_tests/views/__init__.py deleted file mode 100644 index 0982aebd3..000000000 --- a/integration_tests/views/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .async_view import AsyncView -from .sync_view import SyncView - -__all__ = ["SyncView", "AsyncView"] diff --git a/integration_tests/views/async_view.py b/integration_tests/views/async_view.py deleted file mode 100644 index 02d4cf7ff..000000000 --- a/integration_tests/views/async_view.py +++ /dev/null @@ -1,9 +0,0 @@ -from robyn import Request - - -def AsyncView(): - async def get(): - return "Hello, world!" - - async def post(request: Request): - return request.body diff --git a/integration_tests/views/sync_view.py b/integration_tests/views/sync_view.py deleted file mode 100644 index 3b78f82a1..000000000 --- a/integration_tests/views/sync_view.py +++ /dev/null @@ -1,9 +0,0 @@ -from robyn import Request - - -def SyncView(): - def get(): - return "Hello, world!" - - def post(request: Request): - return request.body diff --git a/poetry.lock b/poetry.lock index a0029ddb9..b5b7cddb6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,19 +17,19 @@ test = ["coverage", "flake8", "mypy", "pexpect", "wheel"] [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -85,13 +85,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -197,13 +197,13 @@ unicode-backport = ["unicodedata2"] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -961,13 +961,13 @@ files = [ [[package]] name = "rustimport" -version = "1.5.1" +version = "1.6.0" description = "Import Rust files directly from Python!" optional = false python-versions = "*" files = [ - {file = "rustimport-1.5.1-py3-none-any.whl", hash = "sha256:50aaa149479ea9da19dd23c971b734dd426f8bc732241c92b8fdfcb215bf7e24"}, - {file = "rustimport-1.5.1.tar.gz", hash = "sha256:6349461ab131ccf925b07cf58c9c535525b17e1bbb7582718020c1ec7e043e40"}, + {file = "rustimport-1.6.0-py3-none-any.whl", hash = "sha256:fda7a82bd8c7f5331979d274ea1344a066f2c435bf5ce52294fdc9012f4a9641"}, + {file = "rustimport-1.6.0.tar.gz", hash = "sha256:980f01d6d286c64462edd37cce468227010b0376b4ade13590ed8ab181460355"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index fb99e81a2..6deb9e8c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ classifiers = [ dependencies = [ 'watchdog == 4.0.1', 'multiprocess == 0.70.14', - 'nestd == 0.3.1', 'inquirerpy == 0.3.4', 'rustimport == 1.3.4', 'orjson == 3.9.15', diff --git a/robyn/__init__.py b/robyn/__init__.py index 929c09718..4dfd14a06 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -3,10 +3,9 @@ import os import socket from pathlib import Path -from typing import Callable, List, Optional, Tuple, Union +from typing import Callable, List, Optional, Union import multiprocess as mp # type: ignore -from nestd import get_all_nested # type: ignore from robyn import status_codes from robyn.argument_parser import Config @@ -307,49 +306,6 @@ def start(self, host: str = "127.0.0.1", port: int = 8080, _check_port: bool = T def exception(self, exception_handler: Callable): self.exception_handler = exception_handler - def add_view(self, endpoint: str, view: Callable, const: bool = False): - """ - This is base handler for the view decorators - - :param endpoint str: endpoint for the route added - :param handler function: represents the function passed as a parent handler for single route with different route types - """ - http_methods = { - "GET": HttpMethod.GET, - "POST": HttpMethod.POST, - "PUT": HttpMethod.PUT, - "DELETE": HttpMethod.DELETE, - "PATCH": HttpMethod.PATCH, - "HEAD": HttpMethod.HEAD, - "OPTIONS": HttpMethod.OPTIONS, - } - - def get_functions(view) -> List[Tuple[HttpMethod, Callable]]: - functions = get_all_nested(view) - output = [] - for name, handler in functions: - route_type = name.upper() - method = http_methods.get(route_type) - if method is not None: - output.append((method, handler)) - return output - - handlers = get_functions(view) - for route_type, handler in handlers: - self.add_route(route_type, endpoint, handler, const) - - def view(self, endpoint: str, const: bool = False): - """ - The @app.view decorator to add a view with the GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS method - - :param endpoint str: endpoint to server the route - """ - - def inner(handler): - return self.add_view(endpoint, handler, const) - - return inner - def get( self, endpoint: str, From 2aad774d9648ae1c31baa6d8a98841b378e4b61d Mon Sep 17 00:00:00 2001 From: Dave Warnock Date: Tue, 31 Dec 2024 04:14:40 +0000 Subject: [PATCH 2/5] refactor: openApi made lazy. (#1066) * refactor: openApi made lazy. This is a staging post so there are extra tests (in all the decorators). Fixes a potential issue in __init__ where the default argument creates a shared object at parse time rather than a separate one for each instance. As only one instance, not normally an issue but not good practice. One FIXME is an edge case in init_openapi where neither if clause evaluates to True for openapi_file_path (should we log an error or what?) * refactor: openApi made lazy. All add_route methods have auth_required, openapi_name and openapi_tags which get stored in the route. Instead of incrementally adding them to openapi routes they are added all at once in app.start include_routes now additionally tracks the list of routers whose routes have been added to the main app. This will be used more later to avoid merging routes until app.start for more flexibility * refactor: try to solve the speed issue by improving handling moving from allowing None for openApi_tags to not * refactor: slightly optimised lower_http_method I've tested with 100,000 loops. This is marginally faster. This PR lowers slows full test suite by 0.004s on my system. However, those benchmarks are mostly setup. Once we are making calls to a running server none of this code is being executed. * refactor: remove extra Robyn instance variable to avoid extra side effects in the future. No consistent peformance impact on 100,000 Robyn(__file__) calls --- robyn/__init__.py | 77 ++++++++++++++++++++++++++------------------ robyn/processpool.py | 2 +- robyn/router.py | 24 ++++++++++++-- 3 files changed, 68 insertions(+), 35 deletions(-) diff --git a/robyn/__init__.py b/robyn/__init__.py index 4dfd14a06..27878d806 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -42,7 +42,7 @@ def __init__( file_object: str, config: Config = Config(), openapi_file_path: Optional[str] = None, - openapi: OpenAPI = OpenAPI(), + openapi: Optional[OpenAPI] = None, dependencies: DependencyMap = DependencyMap(), ) -> None: directory_path = os.path.dirname(os.path.abspath(file_object)) @@ -52,10 +52,7 @@ def __init__( self.dependencies = dependencies self.openapi = openapi - if openapi_file_path: - openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path)) - elif Path(self.directory_path).joinpath("openapi.json").exists(): - openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json")) + self.init_openapi(openapi_file_path) if not bool(os.environ.get("ROBYN_CLI", False)): # the env variables are already set when are running through the cli @@ -81,6 +78,20 @@ def __init__( self.event_handlers: dict = {} self.exception_handler: Optional[Callable] = None self.authentication_handler: Optional[AuthenticationHandler] = None + self.included_routers: List[Router] = [] + + def init_openapi(self, openapi_file_path: Optional[str]) -> None: + if self.config.disable_openapi: + return + + if self.openapi is None: + self.openapi = OpenAPI() + + if openapi_file_path: + self.openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path)) + elif Path(self.directory_path).joinpath("openapi.json").exists(): + self.openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json")) + # TODO! what about when the elif fails? def _handle_dev_mode(self): cli_dev_mode = self.config.dev # --dev @@ -101,6 +112,8 @@ def add_route( handler: Callable, is_const: bool = False, auth_required: bool = False, + openapi_name: str = "", + openapi_tags: Union[List[str], None] = None, ): """ Connect a URI to a handler @@ -116,6 +129,8 @@ def add_route( """ injected_dependencies = self.dependencies.get_dependency_map(self) + list_openapi_tags: List[str] = openapi_tags if openapi_tags else [] + if auth_required: self.middleware_router.add_auth_middleware(endpoint)(handler) @@ -136,6 +151,9 @@ def add_route( endpoint=endpoint, handler=handler, is_const=is_const, + auth_required=auth_required, + openapi_name=openapi_name, + openapi_tags=list_openapi_tags, exception_handler=self.exception_handler, injected_dependencies=injected_dependencies, ) @@ -239,6 +257,15 @@ def is_port_in_use(self, port: int) -> bool: raise Exception(f"Invalid port number: {port}") def _add_openapi_routes(self, auth_required: bool = False): + if self.config.disable_openapi: + return + + if self.openapi is None: + logger.error("No openAPI") + return + + self.router.prepare_routes_openapi(self.openapi, self.included_routers) + self.add_route( route_type=HttpMethod.GET, endpoint="/openapi.json", @@ -325,9 +352,7 @@ def get( """ def inner(handler): - self.openapi.add_openapi_path_obj("get", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required) + return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required, openapi_name, openapi_tags) return inner @@ -348,9 +373,7 @@ def post( """ def inner(handler): - self.openapi.add_openapi_path_obj("post", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -371,9 +394,7 @@ def put( """ def inner(handler): - self.openapi.add_openapi_path_obj("put", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -394,9 +415,7 @@ def delete( """ def inner(handler): - self.openapi.add_openapi_path_obj("delete", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -417,9 +436,7 @@ def patch( """ def inner(handler): - self.openapi.add_openapi_path_obj("patch", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -440,9 +457,7 @@ def head( """ def inner(handler): - self.openapi.add_openapi_path_obj("head", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -463,9 +478,7 @@ def options( """ def inner(handler): - self.openapi.add_openapi_path_obj("options", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -486,8 +499,7 @@ def connect( """ def inner(handler): - self.openapi.add_openapi_path_obj("connect", endpoint, openapi_name, openapi_tags, handler) - return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -508,9 +520,7 @@ def trace( """ def inner(handler): - self.openapi.add_openapi_path_obj("trace", endpoint, openapi_name, openapi_tags, handler) - - return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required) + return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) return inner @@ -520,11 +530,14 @@ def include_router(self, router): :param router Robyn: the router object to include the routes from """ + self.included_routers.append(router) + self.router.routes.extend(router.router.routes) self.middleware_router.global_middlewares.extend(router.middleware_router.global_middlewares) self.middleware_router.route_middlewares.extend(router.middleware_router.route_middlewares) - self.openapi.add_subrouter_paths(router.openapi) + if not self.config.disable_openapi and self.openapi is not None: + self.openapi.add_subrouter_paths(self.openapi) # extend the websocket routes prefix = router.prefix diff --git a/robyn/processpool.py b/robyn/processpool.py index f1b0839f6..7669ec85c 100644 --- a/robyn/processpool.py +++ b/robyn/processpool.py @@ -182,7 +182,7 @@ def spawn_process( server.set_response_headers_exclude_paths(excluded_response_headers_paths) for route in routes: - route_type, endpoint, function, is_const = route + route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags = route server.add_route(route_type, endpoint, function, is_const) for middleware_type, middleware_function in global_middlewares: diff --git a/robyn/router.py b/robyn/router.py index 655aa5258..774237fe1 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -11,6 +11,7 @@ from robyn.authentication import AuthenticationHandler, AuthenticationNotConfiguredError from robyn.dependency_injection import DependencyMap from robyn.jsonify import jsonify +from robyn.openapi import OpenAPI from robyn.responses import FileResponse from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url from robyn.types import Body, Files, FormData, IPAddress, Method, PathParams @@ -19,11 +20,18 @@ _logger = logging.getLogger(__name__) +def lower_http_method(method: HttpMethod): + return (str(method))[11:].lower() + + class Route(NamedTuple): route_type: HttpMethod route: str function: FunctionInfo is_const: bool + auth_required: bool + openapi_name: str + openapi_tags: List[str] class RouteMiddleware(NamedTuple): @@ -108,6 +116,9 @@ def add_route( # type: ignore endpoint: str, handler: Callable, is_const: bool, + auth_required: bool, + openapi_name: str, + openapi_tags: List[str], exception_handler: Optional[Callable], injected_dependencies: dict, ) -> Union[Callable, CoroutineType]: @@ -226,7 +237,7 @@ def inner_handler(*args, **kwargs): params, new_injected_dependencies, ) - self.routes.append(Route(route_type, endpoint, function, is_const)) + self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags)) return async_inner_handler else: function = FunctionInfo( @@ -236,9 +247,18 @@ def inner_handler(*args, **kwargs): params, new_injected_dependencies, ) - self.routes.append(Route(route_type, endpoint, function, is_const)) + self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags)) return inner_handler + def prepare_routes_openapi(self, openapi: OpenAPI, included_routers: List) -> None: + for route in self.routes: + openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler) + + # TODO! after include_routes does not immediatelly merge all the routes + # for router in included_routers: + # for route in router: + # openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler) + def get_routes(self) -> List[Route]: return self.routes From 0b766c984d8fdf60a473462a28df6adc50f9e66a Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Wed, 1 Jan 2025 23:32:55 +0000 Subject: [PATCH 3/5] Release v0.65.0 --- .github/workflows/release-CI.yml | 18 ++-- Cargo.lock | 2 +- Cargo.toml | 2 +- poetry.lock | 158 +++++++++++++++---------------- pyproject.toml | 4 +- 5 files changed, 90 insertions(+), 94 deletions(-) diff --git a/.github/workflows/release-CI.yml b/.github/workflows/release-CI.yml index 20494ae19..53d14fac3 100644 --- a/.github/workflows/release-CI.yml +++ b/.github/workflows/release-CI.yml @@ -130,27 +130,23 @@ jobs: name: Install build wheel with: arch: ${{ matrix.target }} - distro: ubuntu20.04 + distro: ubuntu22.04 githubToken: ${{ github.token }} # Mount the dist directory as /artifacts in the container dockerRunArgs: | --volume "${PWD}/dist:/artifacts" install: | apt update -y - apt install -y gcc musl-dev python3-dev # this is needed for psutil - apt install -y --no-install-recommends software-properties-common - add-apt-repository ppa:deadsnakes/ppa + apt install -y software-properties-common + add-apt-repository -y ppa:deadsnakes/ppa apt update -y - PYTHON=python${{ matrix.python.version }} - apt install -y $PYTHON $PYTHON-distutils $PYTHON-venv + apt install -y gcc musl-dev python3-dev python${{ matrix.python.version }} python${{ matrix.python.version }}-venv run: | ls -lrth /artifacts - PYTHON=python${{ matrix.python.version }} - $PYTHON --version - $PYTHON -m venv venv + python${{ matrix.python.version }} -m venv venv source venv/bin/activate - pip install --upgrade pip setuptools wheel - pip install --force-reinstall dist/robyn*.whl + python -m pip install --upgrade pip setuptools wheel + python -m pip install --force-reinstall /artifacts/robyn*.whl cd ~ && python -c 'import robyn' - name: Upload wheels uses: actions/upload-artifact@v3 diff --git a/Cargo.lock b/Cargo.lock index 890a30b9f..b84fb5bfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,7 +1481,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.64.2" +version = "0.65.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index a7aaf45be..a0c91a254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.64.2" +version = "0.65.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/poetry.lock b/poetry.lock index b5b7cddb6..6f6a4a376 100644 --- a/poetry.lock +++ b/poetry.lock @@ -330,13 +330,13 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "identify" -version = "2.6.3" +version = "2.6.4" description = "File identification library for Python" optional = false python-versions = ">=3.9" files = [ - {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, - {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, + {file = "identify-2.6.4-py2.py3-none-any.whl", hash = "sha256:993b0f01b97e0568c179bb9196391ff391bfb88a99099dbf5ce392b68f42d0af"}, + {file = "identify-2.6.4.tar.gz", hash = "sha256:285a7d27e397652e8cafe537a6cc97dd470a970f48fb2e9d979aa38eae5513ac"}, ] [package.extras] @@ -599,86 +599,86 @@ tox-to-nox = ["jinja2", "tox (<4)"] [[package]] name = "orjson" -version = "3.10.12" +version = "3.10.13" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"}, - {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"}, - {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"}, - {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"}, - {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"}, - {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"}, - {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"}, - {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"}, - {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"}, - {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"}, - {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"}, - {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"}, - {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"}, - {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"}, - {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"}, - {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"}, - {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"}, - {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"}, - {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"}, - {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"}, - {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"}, + {file = "orjson-3.10.13-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1232c5e873a4d1638ef957c5564b4b0d6f2a6ab9e207a9b3de9de05a09d1d920"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26a0eca3035619fa366cbaf49af704c7cb1d4a0e6c79eced9f6a3f2437964b6"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d4b6acd7c9c829895e50d385a357d4b8c3fafc19c5989da2bae11783b0fd4977"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1884e53c6818686891cc6fc5a3a2540f2f35e8c76eac8dc3b40480fb59660b00"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a428afb5720f12892f64920acd2eeb4d996595bf168a26dd9190115dbf1130d"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba5b13b8739ce5b630c65cb1c85aedbd257bcc2b9c256b06ab2605209af75a2e"}, + {file = "orjson-3.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cab83e67f6aabda1b45882254b2598b48b80ecc112968fc6483fa6dae609e9f0"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62c3cc00c7e776c71c6b7b9c48c5d2701d4c04e7d1d7cdee3572998ee6dc57cc"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dc03db4922e75bbc870b03fc49734cefbd50fe975e0878327d200022210b82d8"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22f1c9a30b43d14a041a6ea190d9eca8a6b80c4beb0e8b67602c82d30d6eec3e"}, + {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b42f56821c29e697c68d7d421410d7c1d8f064ae288b525af6a50cf99a4b1200"}, + {file = "orjson-3.10.13-cp310-cp310-win32.whl", hash = "sha256:0dbf3b97e52e093d7c3e93eb5eb5b31dc7535b33c2ad56872c83f0160f943487"}, + {file = "orjson-3.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:46c249b4e934453be4ff2e518cd1adcd90467da7391c7a79eaf2fbb79c51e8c7"}, + {file = "orjson-3.10.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a36c0d48d2f084c800763473020a12976996f1109e2fcb66cfea442fdf88047f"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0065896f85d9497990731dfd4a9991a45b0a524baec42ef0a63c34630ee26fd6"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92b4ec30d6025a9dcdfe0df77063cbce238c08d0404471ed7a79f309364a3d19"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a94542d12271c30044dadad1125ee060e7a2048b6c7034e432e116077e1d13d2"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3723e137772639af8adb68230f2aa4bcb27c48b3335b1b1e2d49328fed5e244c"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f00c7fb18843bad2ac42dc1ce6dd214a083c53f1e324a0fd1c8137c6436269b"}, + {file = "orjson-3.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0e2759d3172300b2f892dee85500b22fca5ac49e0c42cfff101aaf9c12ac9617"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee948c6c01f6b337589c88f8e0bb11e78d32a15848b8b53d3f3b6fea48842c12"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa6fe68f0981fba0d4bf9cdc666d297a7cdba0f1b380dcd075a9a3dd5649a69e"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbcd7aad6bcff258f6896abfbc177d54d9b18149c4c561114f47ebfe74ae6bfd"}, + {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2149e2fcd084c3fd584881c7f9d7f9e5ad1e2e006609d8b80649655e0d52cd02"}, + {file = "orjson-3.10.13-cp311-cp311-win32.whl", hash = "sha256:89367767ed27b33c25c026696507c76e3d01958406f51d3a2239fe9e91959df2"}, + {file = "orjson-3.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:dca1d20f1af0daff511f6e26a27354a424f0b5cf00e04280279316df0f604a6f"}, + {file = "orjson-3.10.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a3614b00621c77f3f6487792238f9ed1dd8a42f2ec0e6540ee34c2d4e6db813a"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c976bad3996aa027cd3aef78aa57873f3c959b6c38719de9724b71bdc7bd14b"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f74d878d1efb97a930b8a9f9898890067707d683eb5c7e20730030ecb3fb930"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33ef84f7e9513fb13b3999c2a64b9ca9c8143f3da9722fbf9c9ce51ce0d8076e"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd2bcde107221bb9c2fa0c4aaba735a537225104173d7e19cf73f70b3126c993"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:064b9dbb0217fd64a8d016a8929f2fae6f3312d55ab3036b00b1d17399ab2f3e"}, + {file = "orjson-3.10.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0044b0b8c85a565e7c3ce0a72acc5d35cda60793edf871ed94711e712cb637d"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7184f608ad563032e398f311910bc536e62b9fbdca2041be889afcbc39500de8"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d36f689e7e1b9b6fb39dbdebc16a6f07cbe994d3644fb1c22953020fc575935f"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54433e421618cd5873e51c0e9d0b9fb35f7bf76eb31c8eab20b3595bb713cd3d"}, + {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1ba0c5857dd743438acecc1cd0e1adf83f0a81fee558e32b2b36f89e40cee8b"}, + {file = "orjson-3.10.13-cp312-cp312-win32.whl", hash = "sha256:a42b9fe4b0114b51eb5cdf9887d8c94447bc59df6dbb9c5884434eab947888d8"}, + {file = "orjson-3.10.13-cp312-cp312-win_amd64.whl", hash = "sha256:3a7df63076435f39ec024bdfeb4c9767ebe7b49abc4949068d61cf4857fa6d6c"}, + {file = "orjson-3.10.13-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2cdaf8b028a976ebab837a2c27b82810f7fc76ed9fb243755ba650cc83d07730"}, + {file = "orjson-3.10.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a946796e390cbb803e069472de37f192b7a80f4ac82e16d6eb9909d9e39d56"}, + {file = "orjson-3.10.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d64f1db5ecbc21eb83097e5236d6ab7e86092c1cd4c216c02533332951afc"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:711878da48f89df194edd2ba603ad42e7afed74abcd2bac164685e7ec15f96de"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cf16f06cb77ce8baf844bc222dbcb03838f61d0abda2c3341400c2b7604e436e"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8257c3fb8dd7b0b446b5e87bf85a28e4071ac50f8c04b6ce2d38cb4abd7dff57"}, + {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9c3a87abe6f849a4a7ac8a8a1dede6320a4303d5304006b90da7a3cd2b70d2c"}, + {file = "orjson-3.10.13-cp313-cp313-win32.whl", hash = "sha256:527afb6ddb0fa3fe02f5d9fba4920d9d95da58917826a9be93e0242da8abe94a"}, + {file = "orjson-3.10.13-cp313-cp313-win_amd64.whl", hash = "sha256:b5f7c298d4b935b222f52d6c7f2ba5eafb59d690d9a3840b7b5c5cda97f6ec5c"}, + {file = "orjson-3.10.13-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e49333d1038bc03a25fdfe11c86360df9b890354bfe04215f1f54d030f33c342"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:003721c72930dbb973f25c5d8e68d0f023d6ed138b14830cc94e57c6805a2eab"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63664bf12addb318dc8f032160e0f5dc17eb8471c93601e8f5e0d07f95003784"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6066729cf9552d70de297b56556d14b4f49c8f638803ee3c90fd212fa43cc6af"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a1152e2761025c5d13b5e1908d4b1c57f3797ba662e485ae6f26e4e0c466388"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b21d91c5c5ef8a201036d207b1adf3aa596b930b6ca3c71484dd11386cf6c3"}, + {file = "orjson-3.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b12a63f48bb53dba8453d36ca2661f2330126d54e26c1661e550b32864b28ce3"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a5a7624ab4d121c7e035708c8dd1f99c15ff155b69a1c0affc4d9d8b551281ba"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0fee076134398d4e6cb827002468679ad402b22269510cf228301b787fdff5ae"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ae537fcf330b3947e82c6ae4271e092e6cf16b9bc2cef68b14ffd0df1fa8832a"}, + {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f81b26c03f5fb5f0d0ee48d83cea4d7bc5e67e420d209cc1a990f5d1c62f9be0"}, + {file = "orjson-3.10.13-cp38-cp38-win32.whl", hash = "sha256:0bc858086088b39dc622bc8219e73d3f246fb2bce70a6104abd04b3a080a66a8"}, + {file = "orjson-3.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:3ca6f17467ebbd763f8862f1d89384a5051b461bb0e41074f583a0ebd7120e8e"}, + {file = "orjson-3.10.13-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a11532cbfc2f5752c37e84863ef8435b68b0e6d459b329933294f65fa4bda1a"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c96d2fb80467d1d0dfc4d037b4e1c0f84f1fe6229aa7fea3f070083acef7f3d7"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dda4ba4d3e6f6c53b6b9c35266788053b61656a716a7fef5c884629c2a52e7aa"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f998bbf300690be881772ee9c5281eb9c0044e295bcd4722504f5b5c6092ff"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1cc42ed75b585c0c4dc5eb53a90a34ccb493c09a10750d1a1f9b9eff2bd12"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03b0f29d485411e3c13d79604b740b14e4e5fb58811743f6f4f9693ee6480a8f"}, + {file = "orjson-3.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:233aae4474078d82f425134bb6a10fb2b3fc5a1a1b3420c6463ddd1b6a97eda8"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e384e330a67cf52b3597ee2646de63407da6f8fc9e9beec3eaaaef5514c7a1c9"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4222881d0aab76224d7b003a8e5fdae4082e32c86768e0e8652de8afd6c4e2c1"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e400436950ba42110a20c50c80dff4946c8e3ec09abc1c9cf5473467e83fd1c5"}, + {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f47c9e7d224b86ffb086059cdcf634f4b3f32480f9838864aa09022fe2617ce2"}, + {file = "orjson-3.10.13-cp39-cp39-win32.whl", hash = "sha256:a9ecea472f3eb653e1c0a3d68085f031f18fc501ea392b98dcca3e87c24f9ebe"}, + {file = "orjson-3.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:5385935a73adce85cc7faac9d396683fd813566d3857fa95a0b521ef84a5b588"}, + {file = "orjson-3.10.13.tar.gz", hash = "sha256:eb9bfb14ab8f68d9d9492d4817ae497788a15fd7da72e14dfabc289c3bb088ec"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 6deb9e8c3..ce7e69619 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.64.2" +version = "0.65.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] classifiers = [ @@ -46,7 +46,7 @@ Changelog = "https://github.com/sparckles/robyn/blob/main/CHANGELOG.md" [tool.poetry] name = "robyn" -version = "0.64.2" +version = "0.65.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 46d65213ff58a7c641058d4c203894fc458d024b Mon Sep 17 00:00:00 2001 From: Dave Warnock Date: Tue, 7 Jan 2025 02:26:05 +0000 Subject: [PATCH 4/5] refactor: add BaseRobyn class (#1100) * refactor: create BaseRobyn class. Move all Robyn to there apart from start * refactor: make subrouter class of BaseRobyn --- robyn/__init__.py | 103 ++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/robyn/__init__.py b/robyn/__init__.py index 27878d806..ca81226f9 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -2,6 +2,7 @@ import logging import os import socket +from abc import ABC from pathlib import Path from typing import Callable, List, Optional, Union @@ -34,7 +35,7 @@ print("Compiled rust files") -class Robyn: +class BaseRobyn(ABC): """This is the python wrapper for the Robyn binaries.""" def __init__( @@ -282,54 +283,6 @@ def _add_openapi_routes(self, auth_required: bool = False): ) self.exclude_response_headers_for(["/docs", "/openapi.json"]) - def start(self, host: str = "127.0.0.1", port: int = 8080, _check_port: bool = True): - """ - Starts the server - - :param host str: represents the host at which the server is listening - :param port int: represents the port number at which the server is listening - :param _check_port bool: represents if the port should be checked if it is already in use - """ - - host = os.getenv("ROBYN_HOST", host) - port = int(os.getenv("ROBYN_PORT", port)) - open_browser = bool(os.getenv("ROBYN_BROWSER_OPEN", self.config.open_browser)) - - if _check_port: - while self.is_port_in_use(port): - logger.error("Port %s is already in use. Please use a different port.", port) - try: - port = int(input("Enter a different port: ")) - except Exception: - logger.error("Invalid port number. Please enter a valid port number.") - continue - - if not self.config.disable_openapi: - self._add_openapi_routes() - logger.info("Docs hosted at http://%s:%s/docs", host, port) - - logger.info("Robyn version: %s", __version__) - logger.info("Starting server at http://%s:%s", host, port) - - mp.allow_connection_pickling() - - run_processes( - host, - port, - self.directories, - self.request_headers, - self.router.get_routes(), - self.middleware_router.get_global_middlewares(), - self.middleware_router.get_route_middlewares(), - self.web_socket_router.get_routes(), - self.event_handlers, - self.config.workers, - self.config.processes, - self.response_headers, - self.excluded_response_headers_paths, - open_browser, - ) - def exception(self, exception_handler: Callable): self.exception_handler = exception_handler @@ -557,7 +510,57 @@ def configure_authentication(self, authentication_handler: AuthenticationHandler self.middleware_router.set_authentication_handler(authentication_handler) -class SubRouter(Robyn): +class Robyn(BaseRobyn): + def start(self, host: str = "127.0.0.1", port: int = 8080, _check_port: bool = True): + """ + Starts the server + + :param host str: represents the host at which the server is listening + :param port int: represents the port number at which the server is listening + :param _check_port bool: represents if the port should be checked if it is already in use + """ + + host = os.getenv("ROBYN_HOST", host) + port = int(os.getenv("ROBYN_PORT", port)) + open_browser = bool(os.getenv("ROBYN_BROWSER_OPEN", self.config.open_browser)) + + if _check_port: + while self.is_port_in_use(port): + logger.error("Port %s is already in use. Please use a different port.", port) + try: + port = int(input("Enter a different port: ")) + except Exception: + logger.error("Invalid port number. Please enter a valid port number.") + continue + + if not self.config.disable_openapi: + self._add_openapi_routes() + logger.info("Docs hosted at http://%s:%s/docs", host, port) + + logger.info("Robyn version: %s", __version__) + logger.info("Starting server at http://%s:%s", host, port) + + mp.allow_connection_pickling() + + run_processes( + host, + port, + self.directories, + self.request_headers, + self.router.get_routes(), + self.middleware_router.get_global_middlewares(), + self.middleware_router.get_route_middlewares(), + self.web_socket_router.get_routes(), + self.event_handlers, + self.config.workers, + self.config.processes, + self.response_headers, + self.excluded_response_headers_paths, + open_browser, + ) + + +class SubRouter(BaseRobyn): def __init__(self, file_object: str, prefix: str = "", config: Config = Config(), openapi: OpenAPI = OpenAPI()) -> None: super().__init__(file_object=file_object, config=config, openapi=openapi) self.prefix = prefix From c623930ecbfae3ec58f2646fe24723439a4fa9e5 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:57:35 +0000 Subject: [PATCH 5/5] docs: first version of chinese docs (#1102) * docs: first version of chinese docs * fix minor ui bug * fix folder structure * add more docs * add more translation * add more translation --- docs_src/next.config.mjs | 14 ++ .../documentation/LanguageSelector.jsx | 72 ++++++ .../components/documentation/Navigation.jsx | 220 ++++++++++++++---- .../api_reference/advanced_features.mdx | 0 .../{ => en}/api_reference/authentication.mdx | 0 .../{ => en}/api_reference/const_requests.mdx | 0 .../{ => en}/api_reference/cors.mdx | 0 .../api_reference/dependency_injection.mdx | 0 .../{ => en}/api_reference/exceptions.mdx | 0 .../{ => en}/api_reference/file-uploads.mdx | 0 .../{ => en}/api_reference/form_data.mdx | 0 .../{ => en}/api_reference/future-roadmap.mdx | 0 .../api_reference/getting_started.mdx | 0 .../api_reference/graphql-support.mdx | 0 .../{ => en}/api_reference/index.mdx | 0 .../{ => en}/api_reference/middlewares.mdx | 0 .../api_reference/multiprocess_execution.mdx | 0 .../{ => en}/api_reference/openapi.mdx | 0 .../{ => en}/api_reference/redirection.mdx | 0 .../{ => en}/api_reference/request_object.mdx | 0 .../{ => en}/api_reference/robyn_env.mdx | 0 .../{ => en}/api_reference/scaling.mdx | 0 .../{ => en}/api_reference/templating.mdx | 0 .../api_reference/using_rust_directly.mdx | 0 .../{ => en}/api_reference/websockets.mdx | 0 .../en/api_reference/zh/getting_started.mdx | 139 +++++++++++ .../documentation/{ => en}/architecture.mdx | 0 .../{ => en}/community-resources.mdx | 0 .../authentication-middlewares.mdx | 0 .../{ => en}/example_app/authentication.mdx | 0 .../{ => en}/example_app/deployment.mdx | 0 .../{ => en}/example_app/index.mdx | 0 .../{ => en}/example_app/modeling_routes.mdx | 0 .../example_app/monitoring_and_logging.mdx | 0 .../{ => en}/example_app/openapi.mdx | 0 .../example_app/real_time_notifications.mdx | 0 .../example_app/subrouters_and_views.mdx | 0 .../{ => en}/example_app/templates.mdx | 0 .../documentation/en/example_app/zh/index.mdx | 37 +++ .../en/example_app/zh/subrouters.mdx | 45 ++++ .../framework_performance_comparison.mdx | 0 .../pages/documentation/{ => en}/hosting.mdx | 0 .../pages/documentation/{ => en}/index.mdx | 0 .../pages/documentation/{ => en}/plugins.mdx | 0 docs_src/src/pages/documentation/zh/index.mdx | 33 +++ 45 files changed, 517 insertions(+), 43 deletions(-) create mode 100644 docs_src/src/components/documentation/LanguageSelector.jsx rename docs_src/src/pages/documentation/{ => en}/api_reference/advanced_features.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/authentication.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/const_requests.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/cors.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/dependency_injection.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/exceptions.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/file-uploads.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/form_data.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/future-roadmap.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/getting_started.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/graphql-support.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/index.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/middlewares.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/multiprocess_execution.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/openapi.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/redirection.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/request_object.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/robyn_env.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/scaling.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/templating.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/using_rust_directly.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/api_reference/websockets.mdx (100%) create mode 100644 docs_src/src/pages/documentation/en/api_reference/zh/getting_started.mdx rename docs_src/src/pages/documentation/{ => en}/architecture.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/community-resources.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/authentication-middlewares.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/authentication.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/deployment.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/index.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/modeling_routes.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/monitoring_and_logging.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/openapi.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/real_time_notifications.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/subrouters_and_views.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/example_app/templates.mdx (100%) create mode 100644 docs_src/src/pages/documentation/en/example_app/zh/index.mdx create mode 100644 docs_src/src/pages/documentation/en/example_app/zh/subrouters.mdx rename docs_src/src/pages/documentation/{ => en}/framework_performance_comparison.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/hosting.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/index.mdx (100%) rename docs_src/src/pages/documentation/{ => en}/plugins.mdx (100%) create mode 100644 docs_src/src/pages/documentation/zh/index.mdx diff --git a/docs_src/next.config.mjs b/docs_src/next.config.mjs index bdb1d5973..5b59a0fd5 100644 --- a/docs_src/next.config.mjs +++ b/docs_src/next.config.mjs @@ -19,6 +19,20 @@ const nextConfig = { experimental: { scrollRestoration: true, }, + i18n: { + locales: ['en', 'zh'], + defaultLocale: 'en', + localeDetection: false, + }, + async redirects() { + return [ + { + source: '/documentation', + destination: '/documentation/en', + permanent: false, + }, + ] + }, } export default withMDX(nextConfig) diff --git a/docs_src/src/components/documentation/LanguageSelector.jsx b/docs_src/src/components/documentation/LanguageSelector.jsx new file mode 100644 index 000000000..7c0b46402 --- /dev/null +++ b/docs_src/src/components/documentation/LanguageSelector.jsx @@ -0,0 +1,72 @@ +import { useRouter } from 'next/router' +import { Menu } from '@headlessui/react' +import { motion } from 'framer-motion' + +const languages = [ + { code: 'en', name: 'English' }, + { code: 'zh', name: '中文' }, +] + +function LanguageSelector() { + const router = useRouter() + const { pathname, asPath, query } = router + const currentLanguage = asPath.includes('/zh') ? 'zh' : 'en' + + const changeLanguage = (locale) => { + const currentPath = asPath.split('?')[0] + + if (currentPath === '/documentation' || currentPath === '/documentation/') { + router.push(`/documentation/${locale}`) + return + } + + const newPath = currentPath.replace( + /\/documentation\/(en|zh)/, + `/documentation/${locale}` + ) + + router.push(newPath) + } + + return ( + + + {languages.find(l => l.code === currentLanguage)?.name} + + + + + + {languages.map((language) => ( + + {({ active }) => ( + + )} + + ))} + + + ) +} + +export default LanguageSelector \ No newline at end of file diff --git a/docs_src/src/components/documentation/Navigation.jsx b/docs_src/src/components/documentation/Navigation.jsx index 2169baeee..51115fb8e 100644 --- a/docs_src/src/components/documentation/Navigation.jsx +++ b/docs_src/src/components/documentation/Navigation.jsx @@ -4,10 +4,10 @@ import { useRouter } from 'next/router' import clsx from 'clsx' import { AnimatePresence, motion, useIsPresent } from 'framer-motion' -import { Button } from '@/components/documentation/Button' import { useIsInsideMobileNavigation } from '@/components/documentation/MobileNavigation' import { useSectionStore } from '@/components/documentation/SectionProvider' import { Tag } from '@/components/documentation/Tag' +import LanguageSelector from '@/components/documentation/LanguageSelector' import { remToPx } from '@/lib/remToPx' function useInitialValue(value, condition = true) { @@ -187,37 +187,37 @@ export const navigation = [ { title: 'Example Application', links: [ - { title: 'Getting Started', href: '/documentation/example_app' }, + { title: 'Getting Started', href: '/documentation/en/example_app' }, { title: 'Modeling Routes', - href: '/documentation/example_app/modeling_routes', + href: '/documentation/en/example_app/modeling_routes', }, { title: 'Authentication and Authorization', - href: '/documentation/example_app/authentication', + href: '/documentation/en/example_app/authentication', }, { title: 'Middlewares', - href: '/documentation/example_app/authentication-middlewares', + href: '/documentation/en/example_app/authentication-middlewares', }, { title: 'Real Time Notifications', - href: '/documentation/example_app/real_time_notifications', + href: '/documentation/en/example_app/real_time_notifications', }, { title: 'Monitoring and Logging', - href: '/documentation/example_app/monitoring_and_logging', + href: '/documentation/en/example_app/monitoring_and_logging', }, - { title: 'Deployment', href: '/documentation/example_app/deployment' }, + { title: 'Deployment', href: '/documentation/en/example_app/deployment' }, { title: 'OpenAPI Documentation', - href: '/documentation/example_app/openapi', + href: '/documentation/en/example_app/openapi', }, - { title: 'Templates', href: '/documentation/example_app/templates' }, + { title: 'Templates', href: '/documentation/en/example_app/templates' }, { title: 'SubRouters', - href: '/documentation/example_app/subrouters', + href: '/documentation/en/example_app/subrouters', }, ], }, @@ -225,87 +225,87 @@ export const navigation = [ title: 'API Reference', links: [ { - href: '/documentation/api_reference/', + href: '/documentation/en/api_reference/', title: 'Installation', }, { - href: '/documentation/api_reference/getting_started', + href: '/documentation/en/api_reference/getting_started', title: 'Getting Started', }, { - href: '/documentation/api_reference/request_object', + href: '/documentation/en/api_reference/request_object', title: 'The Request Object', }, { - href: '/documentation/api_reference/robyn_env', + href: '/documentation/en/api_reference/robyn_env', title: 'The Robyn Env file', }, { - href: '/documentation/api_reference/middlewares', + href: '/documentation/en/api_reference/middlewares', title: 'Middlewares, Events and Websockets', }, { - href: '/documentation/api_reference/authentication', + href: '/documentation/en/api_reference/authentication', title: 'Authentication', }, { - href: '/documentation/api_reference/const_requests', + href: '/documentation/en/api_reference/const_requests', title: 'Const Requests and Multi Core Scaling', }, { - href: '/documentation/api_reference/cors', + href: '/documentation/en/api_reference/cors', title: 'CORS', }, { - href: '/documentation/api_reference/templating', + href: '/documentation/en/api_reference/templating', title: 'Templating', }, { title: 'Redirection', - href: '/documentation/api_reference/redirection', + href: '/documentation/en/api_reference/redirection', }, { - href: '/documentation/api_reference/file-uploads', + href: '/documentation/en/api_reference/file-uploads', title: 'File Uploads', }, { - href: '/documentation/api_reference/form_data', + href: '/documentation/en/api_reference/form_data', title: 'Form Data', }, { - href: '/documentation/api_reference/websockets', + href: '/documentation/en/api_reference/websockets', title: 'Websockets', }, { - href: '/documentation/api_reference/exceptions', + href: '/documentation/en/api_reference/exceptions', title: 'Exceptions', }, { - href: '/documentation/api_reference/scaling', + href: '/documentation/en/api_reference/scaling', title: 'Scaling the Application', }, { - href: '/documentation/api_reference/advanced_features', + href: '/documentation/en/api_reference/advanced_features', title: 'Advanced Features', }, { - href: '/documentation/api_reference/multiprocess_execution', + href: '/documentation/en/api_reference/multiprocess_execution', title: 'Multiprocess Execution', }, { - href: '/documentation/api_reference/using_rust_directly', + href: '/documentation/en/api_reference/using_rust_directly', title: 'Direct Rust Usage', }, { - href: '/documentation/api_reference/graphql-support', + href: '/documentation/en/api_reference/graphql-support', title: 'GraphQL Support', }, { - href: '/documentation/api_reference/openapi', + href: '/documentation/en/api_reference/openapi', title: 'OpenAPI Documentation', }, { - href: '/documentation/api_reference/dependency_injection', + href: '/documentation/en/api_reference/dependency_injection', title: 'Dependency Injection', } ], @@ -314,11 +314,11 @@ export const navigation = [ title: 'Community Resources', links: [ { - href: '/documentation/community-resources#talks', + href: '/documentation/en/community-resources#talks', title: 'Talks', }, { - href: '/documentation/community-resources#blogs', + href: '/documentation/en/community-resources#blogs', title: 'Blogs', }, ], @@ -327,7 +327,7 @@ export const navigation = [ title: 'Architecture', links: [ { - href: '/documentation/architecture', + href: '/documentation/en/architecture', title: 'Architecture', }, ], @@ -336,7 +336,7 @@ export const navigation = [ title: 'Framework Comparison', links: [ { - href: '/documentation/framework_performance_comparison', + href: '/documentation/en/framework_performance_comparison', title: 'Performance Comparison', }, ], @@ -345,11 +345,11 @@ export const navigation = [ title: 'Hosting', links: [ { - href: '/documentation/hosting#railway', + href: '/documentation/en/hosting#railway', title: 'Railway', }, { - href: '/documentation/hosting#exposing-ports', + href: '/documentation/en/hosting#exposing-ports', title: 'Exposing Ports', }, ], @@ -358,7 +358,7 @@ export const navigation = [ title: 'Plugins', links: [ { - href: '/documentation/plugins', + href: '/documentation/en/plugins', title: 'Plugins', }, ], @@ -367,21 +367,155 @@ export const navigation = [ title: 'Future Roadmap', links: [ { - href: '/documentation/api_reference/future-roadmap', + href: '/documentation/en/api_reference/future-roadmap', title: 'Upcoming Features', }, ], }, ] +// Add translations for navigation titles +const navigationTitles = { + en: { + 'Documentation': 'Documentation', + 'Example Application': 'Example Application', + 'API Reference': 'API Reference', + 'Community Resources': 'Community Resources', + 'Architecture': 'Architecture', + 'Framework Comparison': 'Framework Comparison', + 'Hosting': 'Hosting', + 'Plugins': 'Plugins', + 'Future Roadmap': 'Future Roadmap' + }, + zh: { + 'Documentation': '文档', + 'Example Application': '示例应用', + 'API Reference': 'API 参考', + 'Community Resources': '社区资源', + 'Architecture': '架构', + 'Framework Comparison': '框架对比', + 'Hosting': '托管', + 'Plugins': '插件', + 'Future Roadmap': '未来路线图' + } +} + +// Add translations for navigation titles and link titles +const translations = { + en: { + titles: navigationTitles.en, + links: { + 'Getting Started': 'Getting Started', + 'Modeling Routes': 'Modeling Routes', + 'Authentication and Authorization': 'Authentication and Authorization', + 'Middlewares': 'Middlewares', + 'Real Time Notifications': 'Real Time Notifications', + 'Monitoring and Logging': 'Monitoring and Logging', + 'Deployment': 'Deployment', + 'OpenAPI Documentation': 'OpenAPI Documentation', + 'Templates': 'Templates', + 'SubRouters': 'SubRouters', + 'Installation': 'Installation', + 'The Request Object': 'The Request Object', + 'The Robyn Env file': 'The Robyn Env file', + 'Middlewares, Events and Websockets': 'Middlewares, Events and Websockets', + 'Authentication': 'Authentication', + 'Const Requests and Multi Core Scaling': 'Const Requests and Multi Core Scaling', + 'CORS': 'CORS', + 'Templating': 'Templating', + 'Redirection': 'Redirection', + 'File Uploads': 'File Uploads', + 'Form Data': 'Form Data', + 'Websockets': 'Websockets', + 'Exceptions': 'Exceptions', + 'Scaling the Application': 'Scaling the Application', + 'Advanced Features': 'Advanced Features', + 'Multiprocess Execution': 'Multiprocess Execution', + 'Direct Rust Usage': 'Direct Rust Usage', + 'GraphQL Support': 'GraphQL Support', + 'Dependency Injection': 'Dependency Injection', + 'Talks': 'Talks', + 'Blogs': 'Blogs', + 'Introduction': 'Introduction', + 'Upcoming Features': 'Upcoming Features', + 'Railway': 'Railway', + 'Exposing Ports': 'Exposing Ports' + } + }, + zh: { + titles: navigationTitles.zh, + links: { + 'Getting Started': '开始使用', + 'Modeling Routes': '路由建模', + 'Authentication and Authorization': '身份验证和授权', + 'Middlewares': '中间件', + 'Real Time Notifications': '实时通知', + 'Monitoring and Logging': '监控和日志', + 'Deployment': '部署', + 'OpenAPI Documentation': 'OpenAPI 文档', + 'Templates': '模板', + 'SubRouters': '子路由', + 'Installation': '安装', + 'The Request Object': '请求对象', + 'The Robyn Env file': 'Robyn 环境文件', + 'Middlewares, Events and Websockets': '中间件、事件和 WebSocket', + 'Authentication': '身份验证', + 'Const Requests and Multi Core Scaling': '常量请求和多核心扩展', + 'CORS': '跨域资源共享', + 'Templating': '模板系统', + 'Redirection': '重定向', + 'File Uploads': '文件上传', + 'Form Data': '表单数据', + 'Websockets': 'WebSocket', + 'Exceptions': '异常处理', + 'Scaling the Application': '应用程序扩展', + 'Advanced Features': '高级功能', + 'Multiprocess Execution': '多进程执行', + 'Direct Rust Usage': '直接使用 Rust', + 'GraphQL Support': 'GraphQL 支持', + 'Dependency Injection': '依赖注入', + 'Talks': '演讲', + 'Blogs': '博客', + 'Introduction': '介绍', + 'Upcoming Features': '即将推出的功能', + 'Railway': 'Railway', + 'Exposing Ports': '端口暴露' + } + } +} + export function Navigation(props) { + const router = useRouter() + const currentLanguage = router.asPath.includes('/zh') ? 'zh' : 'en' + + const getLocalizedHref = (href) => { + if (href === '/documentation') { + return `/documentation/${currentLanguage}` + } + return href.replace('/documentation/en/', `/documentation/${currentLanguage}/`) + } + + // Create localized navigation with translated titles and link titles + const localizedNavigation = navigation.map(group => ({ + ...group, + title: translations[currentLanguage].titles[group.title] || group.title, + links: group.links.map(link => ({ + ...link, + title: translations[currentLanguage].links[link.title] || link.title, + href: getLocalizedHref(link.href) + })) + })) + return (