diff --git a/docs_src/public/images/robyn-config/admin/home.png b/docs_src/public/images/robyn-config/admin/home.png new file mode 100644 index 000000000..e2df2bdcf Binary files /dev/null and b/docs_src/public/images/robyn-config/admin/home.png differ diff --git a/docs_src/public/images/robyn-config/admin/login.png b/docs_src/public/images/robyn-config/admin/login.png new file mode 100644 index 000000000..cdf75a771 Binary files /dev/null and b/docs_src/public/images/robyn-config/admin/login.png differ diff --git a/docs_src/public/images/robyn-config/admin/model_edit.png b/docs_src/public/images/robyn-config/admin/model_edit.png new file mode 100644 index 000000000..f5b2da1c4 Binary files /dev/null and b/docs_src/public/images/robyn-config/admin/model_edit.png differ diff --git a/docs_src/public/images/robyn-config/admin/model_list.png b/docs_src/public/images/robyn-config/admin/model_list.png new file mode 100644 index 000000000..ed90a4119 Binary files /dev/null and b/docs_src/public/images/robyn-config/admin/model_list.png differ diff --git a/docs_src/public/images/robyn-config/admin/settings.png b/docs_src/public/images/robyn-config/admin/settings.png new file mode 100644 index 000000000..bfc8f6100 Binary files /dev/null and b/docs_src/public/images/robyn-config/admin/settings.png differ diff --git a/docs_src/public/images/robyn-config/monitoring/logs.png b/docs_src/public/images/robyn-config/monitoring/logs.png new file mode 100644 index 000000000..fb51b383a Binary files /dev/null and b/docs_src/public/images/robyn-config/monitoring/logs.png differ diff --git a/docs_src/public/images/robyn-config/monitoring/metrics.png b/docs_src/public/images/robyn-config/monitoring/metrics.png new file mode 100644 index 000000000..615f42a08 Binary files /dev/null and b/docs_src/public/images/robyn-config/monitoring/metrics.png differ diff --git a/docs_src/src/pages/documentation/en/api_reference/index.mdx b/docs_src/src/pages/documentation/en/api_reference/index.mdx index 637c5b9ad..fbbcdac80 100644 --- a/docs_src/src/pages/documentation/en/api_reference/index.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/index.mdx @@ -5,37 +5,38 @@ Once upon a time in the city of Gotham, there was a powerful superhero named Rob One day, Batman approached Robyn for help with building a web application. Batman had heard about Robyn's powerful features and wanted to harness them to create a remarkable application. Batman was looking for an ally and in Robyn, he found the best one! - ## Installing Robyn - Robyn is a Python library that you can install using `pip` or `conda` - ```bash {{ title: 'pip' }} - pip install robyn - ``` +```bash {{ title: 'pip' }} +pip install robyn +``` + +```bash {{ title: 'conda' }} +conda install robyn -c conda-forge +``` - ```bash {{ title: 'conda' }} - conda install robyn -c conda-forge - ``` -While there are other more extensions of Robyn like +Robyn also ships optional extras for templating, Pydantic validation, project scaffolding with `robyn-config`, and a combined `all` bundle: - ```bash {{ title: 'pip' }} - pip install "robyn[templating]" - ``` - - ```bash {{ title: 'conda' }} - conda install "robyn[templating]" -c conda-forge - ``` - +```bash {{ title: 'pip' }} +pip install "robyn[templating]" +pip install "robyn[pydantic]" +pip install "robyn[robyn-config]" +pip install "robyn[all]" +``` +```bash {{ title: 'conda' }} +conda install "robyn[templating]" -c conda-forge +``` + It is recommended to install the base package first and then install the extensions as needed. @@ -43,8 +44,4 @@ It is recommended to install the base package first and then install the extensi Now, we can start using Robyn to build our application. - - [Getting Started](/documentation/en/api_reference/getting_started) - - - diff --git a/docs_src/src/pages/documentation/en/api_reference/testing.mdx b/docs_src/src/pages/documentation/en/api_reference/testing.mdx index a90c57c67..0825a6df5 100644 --- a/docs_src/src/pages/documentation/en/api_reference/testing.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/testing.mdx @@ -1,5 +1,5 @@ export const description = - 'On this page, we'll explore Robyn\'s built-in TestClient for fast, in-process unit testing without starting a server.' + 'On this page, we\'ll explore Robyn\'s built-in TestClient for fast, in-process unit testing without starting a server.' ## Testing @@ -35,6 +35,7 @@ Import `TestClient` from `robyn.testing`, pass your app to it, and start making assert response.text == "Hello, World!" ``` + @@ -45,13 +46,13 @@ Every request method returns a `TestResponse` with the following properties: -| Property | Type | Description | -| --- | --- | --- | -| `status_code` | `int` | HTTP status code | -| `text` | `str` | Response body as a decoded string | -| `content` | `bytes` | Raw response body | -| `headers` | `Headers` | Response headers | -| `ok` | `bool` | `True` if status is 2xx | +| Property | Type | Description | +| ------------- | --------- | --------------------------------- | +| `status_code` | `int` | HTTP status code | +| `text` | `str` | Response body as a decoded string | +| `content` | `bytes` | Raw response body | +| `headers` | `Headers` | Response headers | +| `ok` | `bool` | `True` if status is 2xx | `TestResponse` also has a `.json()` method that parses the body as JSON. @@ -73,6 +74,7 @@ Every request method returns a `TestResponse` with the following properties: assert data[0]["name"] == "Batman" ``` + @@ -100,6 +102,7 @@ Use `json_data` to send JSON payloads — the client automatically sets `Content assert response.json()["name"] == "Batarang" ``` + @@ -122,10 +125,10 @@ You can also send raw string or bytes bodies, custom headers, query parameters, assert response.ok ``` + - ### Path Parameters Routes with path parameters work exactly as they do in production. The `TestClient` matches the route pattern and extracts parameters automatically. @@ -148,10 +151,10 @@ Path parameters are resolved from the URL and passed to your handler through the assert response.json()["user_id"] == 42 ``` + - ### Testing Middleware The `TestClient` replicates the full request pipeline — before middlewares, the handler, global response headers, and after middlewares — in the same order as the Rust runtime. @@ -185,10 +188,10 @@ Middlewares that modify the request or response are executed just like in produc assert response.headers.get("X-Server") == "Robyn" ``` + - ### Using as a Context Manager `TestClient` implements the context manager protocol. When used with `with`, the internal event loop is automatically cleaned up: @@ -208,10 +211,10 @@ Middlewares that modify the request or response are executed just like in produc # event loop is closed here ``` + - ### Running Tests with pytest Since `TestClient` doesn't start a server, tests run as fast as regular unit tests. Use `pytest` directly — no special plugins or fixtures required. @@ -262,6 +265,7 @@ A typical test file: assert response.status_code == 404 ``` + @@ -271,25 +275,24 @@ Run with: pytest test_app.py -v ``` - ### Available Methods -| Method | Signature | -| --- | --- | -| `client.get(path, **kw)` | GET request | -| `client.post(path, json_data=None, **kw)` | POST request | -| `client.put(path, json_data=None, **kw)` | PUT request | -| `client.patch(path, json_data=None, **kw)` | PATCH request | -| `client.delete(path, json_data=None, **kw)` | DELETE request | -| `client.head(path, **kw)` | HEAD request | -| `client.options(path, **kw)` | OPTIONS request | +| Method | Signature | +| ------------------------------------------- | --------------- | +| `client.get(path, **kw)` | GET request | +| `client.post(path, json_data=None, **kw)` | POST request | +| `client.put(path, json_data=None, **kw)` | PUT request | +| `client.patch(path, json_data=None, **kw)` | PATCH request | +| `client.delete(path, json_data=None, **kw)` | DELETE request | +| `client.head(path, **kw)` | HEAD request | +| `client.options(path, **kw)` | OPTIONS request | All methods accept these keyword arguments: -| Argument | Type | Description | -| --- | --- | --- | -| `body` | `str \| bytes` | Raw request body | -| `headers` | `dict` | Request headers | -| `query_params` | `dict` | Query string parameters | -| `form_data` | `dict` | Form data fields | -| `files` | `dict` | File uploads (name → bytes) | +| Argument | Type | Description | +| -------------- | -------------- | --------------------------- | +| `body` | `str \| bytes` | Raw request body | +| `headers` | `dict` | Request headers | +| `query_params` | `dict` | Query string parameters | +| `form_data` | `dict` | Form data fields | +| `files` | `dict` | File uploads (name → bytes) | diff --git a/docs_src/src/pages/documentation/en/plugins.mdx b/docs_src/src/pages/documentation/en/plugins.mdx index 512336342..c4dd37718 100644 --- a/docs_src/src/pages/documentation/en/plugins.mdx +++ b/docs_src/src/pages/documentation/en/plugins.mdx @@ -1,3 +1,11 @@ +export const description = + 'Robyn plugins, including rate limiting and robyn-config for project scaffolding, admin panels, and observability.' + +export const sections = [ + { title: 'Rate Limit Plugin', id: 'rate-limit-plugin' }, + { title: 'Robyn Config', id: 'robyn-config' }, +] + ## Plugins Robyn is a versatile and extensible web framework that allows anyone to make plugins over the top of Robyn. @@ -34,6 +42,164 @@ In this example, robyn-rate-limits is used to enforce a rate limit of 3 requests The plugin integrates seamlessly with the Robyn web framework, enhancing the security and stability of your application by preventing excessive requests from a single client. +### Robyn Config + +- Description: A CLI companion for bootstrapping and maintaining production-ready Robyn applications. It can create new projects, add business entities, scaffold an admin panel, and wire an observability stack into an existing app. +- GitHub repository: [robyn-config](https://github.com/Lehsqa/robyn-config) +- Installation: + `python -m pip install robyn-config` + + Or as a Robyn optional dependency: + `python -m pip install robyn[robyn-config]` + +- Python support: Python 3.11 or newer. + +#### Create a Project + +```bash +# DDD + SQLAlchemy with uv dependency locking (default) +robyn-config create my-service --design ddd --orm sqlalchemy ./my-service + +# MVC + Tortoise ORM with poetry dependency locking +robyn-config create newsletter --design mvc --orm tortoise --package-manager poetry ./newsletter + +# Interactive project setup +robyn-config create -i +``` + +`robyn-config create` gives you explicit choices for the two main architecture branches: + +- **DDD** for domain, application, infrastructure, and presentation layers. +- **MVC** for views, repositories, models, and URL routing. + +It also lets you choose the database layer and package manager: + +- **SQLAlchemy** or **Tortoise ORM** for persistence. +- **uv** by default, or **poetry** with `--package-manager poetry`. + +#### Add Business Logic + +Inside a generated project, add a new entity and let `robyn-config` place the files in the correct design-specific layers: + +```bash +cd my-service +robyn-config add product +``` + +This generates the model, repository, route/controller code, and application wiring for the selected architecture. Custom generation paths can be configured in `[tool.robyn-config.add]` inside the generated project's `pyproject.toml`. + +#### Add an Admin Panel + +```bash +cd my-service +robyn-config adminpanel + +# Override the default admin/admin bootstrap credentials +robyn-config adminpanel --username superadmin --password super-secret-password ./my-service +``` + +The admin panel scaffolding adds an ORM-aware admin module, registers it with the application, discovers available project models, and exposes CRUD views for those models. + +
+
+ Robyn Config admin login screen +
+ Admin login +
+
+
+ Robyn Config admin home dashboard +
+ Admin dashboard +
+
+
+ Robyn Config admin model table +
+ Model listing +
+
+
+ Robyn Config admin model editor +
+ Model editor +
+
+
+ +#### Add Monitoring + +```bash +cd my-service +robyn-config monitoring + +# Start the application stack and observability stack +docker compose up -d +docker compose -f docker-compose.monitoring.yml up -d +``` + +The monitoring command adds a `/metrics` endpoint, installs `prometheus-client`, and provisions Docker Compose assets for Grafana Alloy, Loki, Prometheus, and Grafana. Grafana is available at `http://localhost:3000` with dashboards for logs and process metrics. + +
+
+ Robyn Config Grafana logs dashboard +
+ Logs dashboard +
+
+
+ Robyn Config Grafana metrics dashboard +
+ Metrics dashboard +
+
+
+ +#### Validate Generated Projects + +After generating or modifying a project, run the checks that match the generated package manager: + +```bash +# uv projects +uv run pytest +uv run ruff check . + +# poetry projects +poetry run pytest +poetry run ruff check . +``` + +For monitoring, confirm the application exposes Prometheus metrics and that Grafana has data: + +```bash +curl http://localhost:8000/metrics +curl -I http://localhost:3000 +``` + ## What's next? After exploring the plugins, Batman wanted to explore the community.So, Robyn pointed him to diff --git a/integration_tests/test_robyn_config.py b/integration_tests/test_robyn_config.py new file mode 100644 index 000000000..2a6b3a86d --- /dev/null +++ b/integration_tests/test_robyn_config.py @@ -0,0 +1,135 @@ +import os +import shutil +import subprocess +import tempfile + +import pytest + +_HAS_ROBYN_CONFIG = shutil.which("robyn-config") is not None + +pytestmark = pytest.mark.skipif(not _HAS_ROBYN_CONFIG, reason="robyn-config not installed") + + +class TestRobynConfigCLI: + """Verify that the robyn-config CLI is accessible and functional.""" + + def test_cli_help_exits_zero(self): + """robyn-config --help should exit with code 0.""" + result = subprocess.run( + ["robyn-config", "--help"], + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0 + assert "Usage" in result.stdout + + def test_cli_create_help(self): + """robyn-config create --help should list available options.""" + result = subprocess.run( + ["robyn-config", "create", "--help"], + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0 + assert "create" in result.stdout.lower() or "Usage" in result.stdout + + def test_cli_add_help(self): + """robyn-config add --help should list available options.""" + result = subprocess.run( + ["robyn-config", "add", "--help"], + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0 + + +class TestRobynConfigScaffolding: + """Integration tests for project scaffolding.""" + + @pytest.fixture + def temp_dir(self): + """Create a temporary directory for scaffolding tests.""" + tmp = tempfile.mkdtemp(prefix="robyn_config_test_") + yield tmp + shutil.rmtree(tmp, ignore_errors=True) + + @pytest.fixture + def scaffolded_project(self): + """Create a scaffolded project for tests that build on top of it.""" + tmp = tempfile.mkdtemp(prefix="robyn_config_scaffolded_") + result = subprocess.run( + ["robyn-config", "create", "testproj", tmp, "--design", "ddd", "--orm", "sqlalchemy"], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, f"Scaffolding failed: {result.stderr}" + yield tmp + shutil.rmtree(tmp, ignore_errors=True) + + @pytest.mark.parametrize("design", ["ddd", "mvc"]) + @pytest.mark.parametrize("orm", ["sqlalchemy", "tortoise"]) + def test_create_project_scaffold(self, temp_dir, design, orm): + """robyn-config create should scaffold a valid project structure.""" + project_name = f"test_project_{design}_{orm}" + + result = subprocess.run( + ["robyn-config", "create", project_name, temp_dir, "--design", design, "--orm", orm], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + + # Verify essential files/directories exist in the scaffolded project + assert os.path.isfile(os.path.join(temp_dir, "pyproject.toml")), "pyproject.toml not found" + assert os.path.isdir(os.path.join(temp_dir, "src")), "src/ directory not found" + assert os.path.isfile(os.path.join(temp_dir, "Makefile")), "Makefile not found" + + def test_adminpanel_scaffolding(self, scaffolded_project): + """robyn-config adminpanel should add admin panel files to an existing project.""" + result = subprocess.run( + ["robyn-config", "adminpanel", scaffolded_project], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + + # Verify admin panel files were created + adminpanel_dir = os.path.join(scaffolded_project, "src", "app", "infrastructure", "adminpanel") + assert os.path.isdir(adminpanel_dir), "adminpanel directory not created" + + def test_adminpanel_with_custom_credentials(self, scaffolded_project): + """robyn-config adminpanel should accept custom username and password.""" + result = subprocess.run( + ["robyn-config", "adminpanel", scaffolded_project, "--username", "testadmin", "--password", "testpass"], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + + adminpanel_dir = os.path.join(scaffolded_project, "src", "app", "infrastructure", "adminpanel") + assert os.path.isdir(adminpanel_dir), "adminpanel directory not created" + + def test_monitoring_scaffolding(self, scaffolded_project): + """robyn-config monitoring should add monitoring pipeline to an existing project.""" + result = subprocess.run( + ["robyn-config", "monitoring", scaffolded_project], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + + # Verify monitoring files were created + assert os.path.isfile(os.path.join(scaffolded_project, "docker-compose.monitoring.yml")), "docker-compose.monitoring.yml not found" + + monitoring_dir = os.path.join(scaffolded_project, "compose", "monitoring") + assert os.path.isdir(monitoring_dir), "compose/monitoring directory not created" + assert os.path.isdir(os.path.join(monitoring_dir, "alloy")), "alloy config not found" + assert os.path.isdir(os.path.join(monitoring_dir, "grafana")), "grafana config not found" diff --git a/pyproject.toml b/pyproject.toml index 4ece61070..d494667a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ dependencies = [ [project.optional-dependencies] "templating" = ["jinja2 >= 3.1.6, < 4.0.0"] "pydantic" = ["pydantic >= 2.0.0, < 3.0.0"] -"all" = ["jinja2 >= 3.1.6, < 4.0.0", "pydantic >= 2.0.0, < 3.0.0"] +"robyn-config" = ["robyn-config >= 1.0.0, < 2.0.0"] +"all" = ["jinja2 >= 3.1.6, < 4.0.0", "pydantic >= 2.0.0, < 3.0.0", "robyn-config >= 1.0.0, < 2.0.0"] [project.urls] Documentation = "https://robyn.tech/" @@ -83,13 +84,15 @@ multiprocess = "^0.70.18" uvloop = { version = "0.22.1", markers = "sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')" } jinja2 = { version = "^3.1.6", optional = true } pydantic = { version = "^2.0.0", optional = true } +robyn-config = { version = "^1.0.0", optional = true } rustimport = "^1.3.4" orjson = "^3.11.5" [tool.poetry.extras] templating = ["jinja2"] pydantic = ["pydantic"] -all = ["jinja2", "pydantic"] +robyn-config = ["robyn-config"] +all = ["jinja2", "pydantic", "robyn-config"] [tool.poetry.group.dev] optional = true