lenapi 0.1.2__tar.gz → 0.1.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lenapi-0.1.2 → lenapi-0.1.4.1}/PKG-INFO +209 -10
- {lenapi-0.1.2 → lenapi-0.1.4.1}/README.md +205 -8
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/__init__.py +3 -2
- lenapi-0.1.4.1/lenapi/admin.py +1017 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/application.py +239 -41
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/authentication.py +51 -8
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/cli.py +338 -89
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/database.py +22 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/docs_guard.py +52 -13
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/models.py +211 -24
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/mongodb.py +16 -2
- lenapi-0.1.4.1/lenapi/project.py +62 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/request.py +14 -4
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/security.py +19 -7
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/security_store.py +17 -4
- lenapi-0.1.4.1/lenapi/transaction.py +76 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi.egg-info/PKG-INFO +209 -10
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi.egg-info/SOURCES.txt +6 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi.egg-info/requires.txt +3 -0
- lenapi-0.1.4.1/lenapi.egg-info/top_level.txt +2 -0
- lenapi-0.1.4.1/lenapi_bootstrap.py +97 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/pyproject.toml +6 -2
- lenapi-0.1.4.1/tests/test_admin_console.py +219 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_framework_features.py +142 -2
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_media_upload.py +20 -0
- lenapi-0.1.4.1/tests/test_project_isolation.py +233 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_user_model_extension.py +7 -0
- lenapi-0.1.2/lenapi.egg-info/top_level.txt +0 -1
- {lenapi-0.1.2 → lenapi-0.1.4.1}/LICENSE +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/auth.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/authorization.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/background.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/concurrency.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/dependencies.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/logging.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/middleware.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/model.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/openapi.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/parameters.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/response.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/sentinels.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/service.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi/throttling.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi.egg-info/dependency_links.txt +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/lenapi.egg-info/entry_points.txt +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/setup.cfg +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_cli_settings_recovery.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_docs_guard.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_mongodb.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_platform_services.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_security_patch.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_sentinels.py +0 -0
- {lenapi-0.1.2 → lenapi-0.1.4.1}/tests/test_throttle.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lenapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4.1
|
|
4
4
|
Summary: A modern ASGI framework with Pydantic validation, sync and async database access, WebSockets, authentication, and integrated API documentation.
|
|
5
5
|
Author: Maekandex Communication Limitation
|
|
6
6
|
Author-email: "Ifeanyi M. Onovo" <ifeanyi@maekandex.com.ng>
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
Project-URL: Homepage, https://github.com/Mikandex/lenapi
|
|
9
9
|
Project-URL: Repository, https://github.com/Mikandex/lenapi
|
|
10
|
-
Project-URL: Documentation, https://
|
|
10
|
+
Project-URL: Documentation, https://lenapi.site/
|
|
11
11
|
Project-URL: Issues, https://github.com/Mikandex/lenapi/issues
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -46,6 +46,8 @@ Provides-Extra: async-mysql
|
|
|
46
46
|
Requires-Dist: asyncmy>=0.2; extra == "async-mysql"
|
|
47
47
|
Provides-Extra: mongodb
|
|
48
48
|
Requires-Dist: pymongo<5,>=4.13; extra == "mongodb"
|
|
49
|
+
Provides-Extra: monitoring
|
|
50
|
+
Requires-Dist: psutil<8,>=5.9; extra == "monitoring"
|
|
49
51
|
Dynamic: license-file
|
|
50
52
|
|
|
51
53
|
# lenApi
|
|
@@ -58,6 +60,18 @@ LenAPI aims to reduce repetitive code and simplify common integrations, helping
|
|
|
58
60
|
|
|
59
61
|
**LenAPI — Compact by Design, Powerful by Capability.**
|
|
60
62
|
|
|
63
|
+
## Feature overview
|
|
64
|
+
|
|
65
|
+
- Typed HTTP routes with Pydantic request and response validation, Swagger UI, and ReDoc.
|
|
66
|
+
- Synchronous and asynchronous ORM operations, migrations, transactions, search, aggregates, and pagination for SQLite, MySQL, and PostgreSQL. MongoDB document support is currently in beta.
|
|
67
|
+
- Authentication building blocks for JWT, API keys, HTTP Basic, sessions, opaque bearer tokens, HMAC, OIDC, and social sign-in.
|
|
68
|
+
- WebSockets for chat, video signaling, JSON, text, and binary messages; webhook handlers with optional HMAC verification; cache and durable background jobs with retries and delayed execution.
|
|
69
|
+
- Validated public and private file uploads, middleware, throttling, and optional gzip compression.
|
|
70
|
+
- An optional model admin console with explicit model/field allow-lists, per-model permissions, and CPU/RAM/process monitoring.
|
|
71
|
+
- Independent generated projects that can run separately or be mounted under a path in a primary project.
|
|
72
|
+
|
|
73
|
+
See the [LenAPI documentation](https://lenapi.site/) for setup guides and runnable examples.
|
|
74
|
+
|
|
61
75
|
## Python versions
|
|
62
76
|
|
|
63
77
|
LenAPI is tested and supported on Python 3.11 through 3.14. Python 3.10 reached
|
|
@@ -411,7 +425,7 @@ app.include_router(api)
|
|
|
411
425
|
|
|
412
426
|
Rate strings accept seconds, minutes, hours, or days, for example `"10/second"`, `"60/minute"`, and `"1000/day"`. Throttling uses a fixed window that starts with the first request. `ip` counts every request from the client address before authentication. `anonymous` counts public calls and, on routes using `auth=`, all requests from that IP before credentials are verified. This intentionally includes successful logins so repeated password checks are stopped before they consume CPU. `authenticated` counts successful route-level authentication by a stable identity returned from `auth=`. Routes using `BasicAuth` receive a default `ip` limit of 10 requests per minute if no pre-auth `ip` or `anonymous` limit is configured. Set `throttle={}` to deliberately disable the inherited and default limit. Route and router `throttle=` values otherwise override inherited limits; an empty mapping disables inherited limits for that route or router. Endpoint-only auth declared with `Depends(...)` does not supply the identity to the authenticated quota; attach it with `auth=` when you want identity-based quotas.
|
|
413
427
|
|
|
414
|
-
When a quota is exceeded, LenAPI returns `429 Too Many Requests` with `Retry-After`, `X-RateLimit-Limit`, and `X-RateLimit-Remaining` headers. Quota counters use the same security backend as IP cooldowns. Choose `SECURITY_STORAGE = "database"` or `"redis"` in project `settings.py`. Database mode stores state in the internal `lenapi_security_store` table or MongoDB collection, initialized by migrations. Redis mode uses `REDIS_URL` and requires `pip install 'lenapi[redis]'` plus a running Redis server. The generated settings default to database storage and include `REDIS_URL = "redis://127.0.0.1:6379/0"` for switching to Redis. Shared database or Redis storage coordinates limits across workers. `SECURITY_STORAGE = "memory"` keeps counters in each process, so use it only for single-process development. LenAPI uses the ASGI connection's client address and does not trust forwarded-IP headers automatically; configure trusted proxy handling at your deployment edge before relying on IP quotas behind a proxy.
|
|
428
|
+
When a quota is exceeded, LenAPI returns `429 Too Many Requests` with `Retry-After`, `X-RateLimit-Limit`, and `X-RateLimit-Remaining` headers. Quota counters use the same security backend as IP cooldowns. Choose `SECURITY_STORAGE = "database"` or `"redis"` in project `settings.py`. Database mode stores state in the internal `lenapi_security_store` table or MongoDB collection, initialized by migrations. Redis mode uses `REDIS_URL` and requires `python -m pip install 'lenapi[redis]'` plus a running Redis server. Run the install command with the same Python environment that starts LenAPI; keep the quotes around the extra in zsh. The generated settings default to database storage and include `REDIS_URL = "redis://127.0.0.1:6379/0"` for switching to Redis. Shared database or Redis storage coordinates limits across workers. `SECURITY_STORAGE = "memory"` keeps counters in each process, so use it only for single-process development. LenAPI uses the ASGI connection's client address and does not trust forwarded-IP headers automatically; configure trusted proxy handling at your deployment edge before relying on IP quotas behind a proxy.
|
|
415
429
|
|
|
416
430
|
For example, app-level limits are inherited by routes that do not set their own throttle. A route-specific mapping replaces the inherited mapping, so repeat any global scopes that you still want on that route:
|
|
417
431
|
|
|
@@ -523,6 +537,8 @@ Run `python lenora.py apikey` inside a project to set `AUTHENTICATION = "api_key
|
|
|
523
537
|
|
|
524
538
|
`settings.py` centralizes the API title/version, docs and OpenAPI URLs, host, port, database selection, token settings, and shared security storage. `HOST = ""` uses `127.0.0.1`; set `HOST` directly to bind another address. `app = LENAPI()` loads project settings automatically. Each generated project gets a unique high-entropy `SECRET_KEY`; `TOKEN` uses it to sign tokens and configures token lifetime and issuer. Keep the key private. Changing it invalidates tokens signed with the old key. `DATABASE_TYPE` chooses one backend (`sqlite`, `mysql`, `postgresql`, or `mongodb`), and `DATABASE_URL` configures it. `ACTIVATE_ASYNC` enables asynchronous access using the same database URL. The default database is SQLite.
|
|
525
539
|
|
|
540
|
+
Generated projects set `DEBUG = True` for local development. Unexpected route or dependency errors then print a traceback to the terminal. LenAPI returns a generic `500 Internal Server Error` response to the client in both debug modes; request validation errors and explicit `HTTPException` responses keep their normal status codes. Set `DEBUG = False` in production to stop LenAPI from printing exception tracebacks. If `DEBUG` is absent, LenAPI defaults to `False`.
|
|
541
|
+
|
|
526
542
|
## Automated tests
|
|
527
543
|
|
|
528
544
|
Pytest is installed with lenApi, so you can run the framework test suite from the repository root:
|
|
@@ -849,6 +865,117 @@ The docs gate only handles `/`, `/logout`, the configured Swagger and ReDoc path
|
|
|
849
865
|
|
|
850
866
|
To remove the gate, set `SAFE_GUARD_DOCS_URL = False` and restart the app. The admin tables may remain in the database; they are not queried while the feature is disabled.
|
|
851
867
|
|
|
868
|
+
### Built-in model admin and resource monitor
|
|
869
|
+
|
|
870
|
+
The optional admin console is separate from the Swagger/ReDoc login and is served at `/admin/`. It uses stateless HTTP Basic authentication for its API requests. Enable it in `settings.py`, then run a migration and create an administrator. The first CLI-created superadmin has full access and can create other admin accounts and grant per-model permissions. If the project already had docs-admin accounts before this feature, create an additional account with the CLI; it becomes superadmin if no superadmin exists yet.
|
|
871
|
+
|
|
872
|
+
```python
|
|
873
|
+
# settings.py
|
|
874
|
+
ADMIN_ENABLED = True
|
|
875
|
+
ADMIN_URL = "/admin/"
|
|
876
|
+
ADMIN_SESSION_LIFETIME_SECONDS = 3600
|
|
877
|
+
ADMIN_COOKIE_SECURE = None # follows the ASGI HTTPS scheme
|
|
878
|
+
|
|
879
|
+
# Monitoring is optional and requires the admin console to be enabled.
|
|
880
|
+
ADMIN_MONITORING_ENABLED = True
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
```bash
|
|
884
|
+
python lenora.py migration
|
|
885
|
+
python lenora.py admin
|
|
886
|
+
python lenora.py run
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
The first SQL migration includes the framework `AdminUser` fields and session storage along with the imported app models. MongoDB initializes the admin collections through `migration`. Mongo collections need a corresponding `MongoDocument` class so the admin can safely render typed edit forms.
|
|
890
|
+
|
|
891
|
+
Create `app/admin.py` to explicitly allow-list models and choose which fields are visible. Once this file exists, only models registered there appear in the console. `fields` selects displayed/editable fields, `readonly` displays selected fields without allowing changes, and `exclude` hides fields. Credential-like fields such as `password`, `api_key`, and `key_digest` remain hidden even if listed; use application routes that hash or rotate credentials.
|
|
892
|
+
|
|
893
|
+
```python
|
|
894
|
+
# app/admin.py
|
|
895
|
+
from lenapi.admin import admin
|
|
896
|
+
from .models import Student, Course, Invoice, Staff
|
|
897
|
+
|
|
898
|
+
admin.register(
|
|
899
|
+
Student,
|
|
900
|
+
fields=("person_name", "course", "email", "profile_image"),
|
|
901
|
+
readonly=("email",),
|
|
902
|
+
exclude=("password", "private_notes"),
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
admin.register(
|
|
906
|
+
Course,
|
|
907
|
+
fields=("title", "description", "duration_weeks", "is_active"),
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
admin.register(
|
|
911
|
+
Invoice,
|
|
912
|
+
fields=("invoice_number", "student_id", "amount", "status", "issued_at"),
|
|
913
|
+
readonly=("invoice_number", "issued_at"),
|
|
914
|
+
exclude=("payment_token", "provider_response"),
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
admin.register(
|
|
918
|
+
Staff,
|
|
919
|
+
fields=("name", "work_email", "department"),
|
|
920
|
+
readonly=("work_email",),
|
|
921
|
+
exclude=("password", "salary", "hr_notes"),
|
|
922
|
+
)
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
When `app/admin.py` is absent, the console keeps its backward-compatible model discovery. Registered models show only the selected columns; readonly fields appear in record forms without editable controls, and excluded fields are omitted from both the UI and the API response. Update requests are checked against the same field allow-list. The console supports list/read, update, and delete; record creation is handled by application endpoints. Superadmins grant `view`, `read`, `write`, and `delete` independently. `view` controls whether a model appears in the sidebar, `read` allows listing records, `write` permits updates, and `delete` controls deletion.
|
|
926
|
+
|
|
927
|
+
Admin accounts and model records use the selected SQL database or MongoDB. Disable the console with `ADMIN_ENABLED = False`; set `ADMIN_MONITORING_ENABLED = False` to hide its monitor. Resource metrics are available only to a superadmin when both settings are enabled:
|
|
928
|
+
|
|
929
|
+
```bash
|
|
930
|
+
python -m pip install 'lenapi[monitoring]'
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
The Resource Monitor page shows CPU and RAM metric cards with rolling graphs, plus the LenAPI process PID and thread count. RAM shows process resident memory and total system-memory use. Metrics refresh every two seconds while the page is open; history remains in that browser page only. The endpoint is restricted to superadmins. This is an operational status view, not a host monitoring or alerting system. Monitoring requires psutil and does not start background sampling.
|
|
934
|
+
|
|
935
|
+
When a main LenAPI project mounts sibling projects, the main project's `/admin/` is the single admin entry point. It groups each mounted project's explicitly registered models under that project's name and reads or updates records through that project's own database. Enable `ADMIN_ENABLED` and create the admin account only in the main project. Keep `ADMIN_ENABLED = False` in mounted projects; they contribute their `app/admin.py` registrations but do not need their own admin login. The mount must be configured with `app.mount(...)` for the main admin to discover that project.
|
|
936
|
+
|
|
937
|
+
In generated projects, prefer a relative model import in `app/admin.py` so project names and mounts remain unambiguous:
|
|
938
|
+
|
|
939
|
+
```python
|
|
940
|
+
from lenapi.admin import admin
|
|
941
|
+
from .models import Student, Course
|
|
942
|
+
|
|
943
|
+
admin.register(Student, fields=("person_name", "course"))
|
|
944
|
+
admin.register(Course, fields=("title", "is_active"))
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Per-model permissions for the main project keep their existing model-name keys. Mounted-project permissions use `<project_name>:<model_name>`, for example `games:student`, so same-named models in different projects stay separate.
|
|
948
|
+
|
|
949
|
+
### Mount independent projects
|
|
950
|
+
|
|
951
|
+
Every generated project keeps its own settings, models, migrations, and database configuration. Run projects independently on different ports, or mount a sibling project into a primary app at any URL prefix you choose. A mount prefix does not need to match the sibling project's name.
|
|
952
|
+
|
|
953
|
+
```python
|
|
954
|
+
# win/main.py — primary application
|
|
955
|
+
from lenapi import LENAPI
|
|
956
|
+
|
|
957
|
+
app = LENAPI()
|
|
958
|
+
app.mount("/gamer", "games")
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
The sibling project must be in the same parent directory and have its own generated `settings.py` with a `PROJECT_NAME`:
|
|
962
|
+
|
|
963
|
+
```text
|
|
964
|
+
workspace/
|
|
965
|
+
├── win/
|
|
966
|
+
│ ├── main.py
|
|
967
|
+
│ ├── settings.py
|
|
968
|
+
│ └── app/
|
|
969
|
+
└── games/
|
|
970
|
+
├── main.py
|
|
971
|
+
├── settings.py
|
|
972
|
+
└── app/
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
The mounted app keeps its own database URL and project namespace. Its routes are reachable under `/gamer/`, while its own docs paths are under `/gamer/docs/` and `/gamer/redoc/`. Project table names include the project's namespace so separate apps sharing one database do not collide. Mounting is optional; independent projects can instead run on separate ports or deployments.
|
|
976
|
+
|
|
977
|
+
For the shared admin console, enable `ADMIN_ENABLED` and create the admin account only in the primary project. Register models explicitly in each project's `app/admin.py`; after mounting, the primary `/admin/` groups those models by project and performs their data operations against each project's configured database. Keep `ADMIN_ENABLED = False` in mounted projects. Each mounted app instance can belong to only one parent admin.
|
|
978
|
+
|
|
852
979
|
The sample app exposes the model queries as HTTP routes: `POST /classrooms` creates a classroom, `GET /classrooms` lists them, and `POST /students` accepts an optional `classroom_id`. `POST /students/batch` creates multiple students, `PUT /students/batch` updates multiple students by ID, and `DELETE /students/batch` deletes multiple students by ID. Batch requests are all-or-nothing: unknown IDs, duplicate names, or a protected deletion reject the whole operation. `PUT /students/{student_id}` updates one student; it returns HTTP 404 if the ID does not exist. `GET /students/{student_id}/classroom` returns the related classroom. Other student routes list, search, and check existence. These routes appear in Swagger at `/docs/`.
|
|
853
980
|
|
|
854
981
|
Batch request examples:
|
|
@@ -962,7 +1089,31 @@ app.include_router(public)
|
|
|
962
1089
|
|
|
963
1090
|
The framework-backed JWT authenticator checks the submitted username and password against the database `User` model and its stored password hash. `POST /auth/token` accepts `{"username": "...", "password": "..."}` and returns an access token plus a refresh token. Send the access token as `Authorization: Bearer <access_token>`. `POST /auth/refresh` accepts `{"refresh_token": "..."}` and returns replacement tokens. The token and refresh routes are intentionally hidden from Swagger; protected API routes still show their Bearer scheme and Swagger's **Authorize** control. These routes have a default IP limit of 10 requests per minute, including failed credential checks; pass `throttle={"ip": "5/minute"}` to `jwt.router(...)` for a different limit. In production, use shared `SECURITY_STORAGE = "database"` or `"redis"` for refresh replay protection across workers. Refresh tokens are single-use; the in-memory fallback coordinates only within one process.
|
|
964
1091
|
|
|
965
|
-
For a custom user table, create the JWT dependency with `authentication("jwt", user_model=YourUser)`. Basic and JWT can use your model as long as it has the selected identifier column and a `password` column containing LenAPI-compatible password hashes. For Basic, select the identifier with `BASIC_AUTH_FIELD`.
|
|
1092
|
+
For a custom user table, create the JWT dependency with `authentication("jwt", user_model=YourUser)`. Basic and JWT can use your model as long as it has the selected identifier column and a `password` column containing LenAPI-compatible password hashes. For Basic, select the identifier with `BASIC_AUTH_FIELD`. JWT defaults to the `username` column; pass `identifier_field="email"` (or another model column) to use a different login identifier. That field name is also used in the `/auth/token` JSON body. Pass the actual `LENAPI` instance with `app=app` when you want refresh-token replay protection to use its shared security store; do not pass a `Router` there.
|
|
1093
|
+
|
|
1094
|
+
For example, a custom model with `email` and a hashed `password` can sign in using `{"email": "ada@example.com", "password": "..."}`:
|
|
1095
|
+
|
|
1096
|
+
```python
|
|
1097
|
+
from lenapi_bootstrap import create_app
|
|
1098
|
+
from lenapi import Depends, authentication
|
|
1099
|
+
|
|
1100
|
+
app = create_app(__file__, __package__)
|
|
1101
|
+
from app.models import Customer
|
|
1102
|
+
auth = authentication(
|
|
1103
|
+
"jwt",
|
|
1104
|
+
user_model=Customer,
|
|
1105
|
+
identifier_field="email",
|
|
1106
|
+
app=app,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
app.include_router(auth.router()) # POST /auth/token and POST /auth/refresh
|
|
1110
|
+
|
|
1111
|
+
@app.get("/profile", auth=auth)
|
|
1112
|
+
def profile(identity=Depends(auth)):
|
|
1113
|
+
return {"customer_id": identity["sub"]}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
`Customer` must define an `email` column and a `password` column containing a LenAPI-compatible password hash. Your registration or provisioning flow is responsible for collecting the email and storing the password hash. `JWTAuth` signs and verifies HS256 tokens, checks expiry and configured issuer, and rejects refresh tokens when used as access tokens. Keep the signing key secret and serve credentials only over HTTPS.
|
|
966
1117
|
|
|
967
1118
|
## API-key authentication
|
|
968
1119
|
|
|
@@ -1625,7 +1776,7 @@ async def billing_webhook(
|
|
|
1625
1776
|
|
|
1626
1777
|
## Databases
|
|
1627
1778
|
|
|
1628
|
-
`DATABASE_TYPE` chooses one backend and `DATABASE_URL` connects to it. The generated project defaults to SQLite. SQLite, MySQL, and PostgreSQL use SQLAlchemy models and Alembic revisions; MongoDB uses the separate `MongoDocument` API and does not have SQL schema revisions. `ACTIVATE_ASYNC = True` enables async access to the same database URL. Keep production credentials out of committed settings and URL-encode reserved characters in credentials.
|
|
1779
|
+
`DATABASE_TYPE` chooses one backend and `DATABASE_URL` connects to it. The generated project defaults to SQLite. SQLite, MySQL, and PostgreSQL use SQLAlchemy models and Alembic revisions; MongoDB uses the separate `MongoDocument` API and does not have SQL schema revisions. `app.database_type` reports the selected backend at runtime. `ACTIVATE_ASYNC = True` enables async access to the same database URL. Keep production credentials out of committed settings and URL-encode reserved characters in credentials.
|
|
1629
1780
|
|
|
1630
1781
|
```python
|
|
1631
1782
|
# settings.py — choose one backend and its URL
|
|
@@ -1643,6 +1794,8 @@ DATABASE_URL = f"sqlite:///{BASE_DIR / 'lenapi.db'}"
|
|
|
1643
1794
|
# MongoDB: install `python -m pip install 'lenapi[mongodb]'`
|
|
1644
1795
|
# DATABASE_TYPE = "mongodb"
|
|
1645
1796
|
# DATABASE_URL = "mongodb://user:password@localhost:27017/app_db"
|
|
1797
|
+
# If the URI has no database path, set this explicitly instead:
|
|
1798
|
+
# MONGODB_DATABASE = "app_db"
|
|
1646
1799
|
|
|
1647
1800
|
ACTIVATE_ASYNC = False
|
|
1648
1801
|
```
|
|
@@ -1686,7 +1839,44 @@ await student.asave()
|
|
|
1686
1839
|
await student.adelete()
|
|
1687
1840
|
```
|
|
1688
1841
|
|
|
1689
|
-
`MongoDatabase.ping()` and `await MongoDatabase.aping()` check connectivity. To select MongoDB, set `DATABASE_TYPE = "mongodb"
|
|
1842
|
+
`MongoDatabase.ping()` and `await MongoDatabase.aping()` check connectivity. To select MongoDB, set `DATABASE_TYPE = "mongodb"`. Put the database name in the URI path, such as `mongodb://localhost:27017/my_database`, or set `MONGODB_DATABASE = "my_database"` when the URI has no path. `ACTIVATE_ASYNC` controls whether LenApi also creates PyMongo's async client. `migration` and `migrate` ensure collections exist for imported `MongoDocument` models and framework security/admin state; document fields are schema-flexible and do not use Alembic revisions. `SECURITY_STORAGE = "database"` uses MongoDB for shared security state, while Redis and process memory remain optional choices. MongoDB uses the official synchronous and `AsyncMongoClient` APIs; see the [PyMongo driver guide](https://www.mongodb.com/docs/languages/python/pymongo-driver/current/).
|
|
1843
|
+
|
|
1844
|
+
#### Switching an existing project from SQL to MongoDB
|
|
1845
|
+
|
|
1846
|
+
Changing `DATABASE_TYPE` and `DATABASE_URL` selects MongoDB for the app; it does not convert existing SQLAlchemy models, tables, or rows. Define the Mongo-backed version as a `MongoDocument`, bind it after creating the app, and update routes to use that document class. If a SQL `model.Model` query runs while MongoDB is selected, LenAPI raises a clear backend-mismatch error explaining that a Mongo document is required. Check the active backend with `app.database_type`.
|
|
1847
|
+
|
|
1848
|
+
```python
|
|
1849
|
+
# settings.py
|
|
1850
|
+
DATABASE_TYPE = "mongodb"
|
|
1851
|
+
DATABASE_URL = "mongodb://127.0.0.1:27017/my_app"
|
|
1852
|
+
|
|
1853
|
+
# app/models.py
|
|
1854
|
+
from lenapi import MongoDocument
|
|
1855
|
+
|
|
1856
|
+
class MongoStudent(MongoDocument):
|
|
1857
|
+
collection_name = "students"
|
|
1858
|
+
username: str
|
|
1859
|
+
course: str
|
|
1860
|
+
|
|
1861
|
+
# main.py
|
|
1862
|
+
from lenapi import LENAPI
|
|
1863
|
+
from pydantic import BaseModel
|
|
1864
|
+
from app.models import MongoStudent
|
|
1865
|
+
|
|
1866
|
+
class StudentCreate(BaseModel):
|
|
1867
|
+
username: str
|
|
1868
|
+
course: str
|
|
1869
|
+
|
|
1870
|
+
app = LENAPI()
|
|
1871
|
+
MongoStudent.bind(app.mongodb)
|
|
1872
|
+
print(app.database_type) # mongodb
|
|
1873
|
+
|
|
1874
|
+
@app.post("/students")
|
|
1875
|
+
def create_student(data: StudentCreate):
|
|
1876
|
+
return MongoStudent.create(**data.model_dump())
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
If you need to carry existing records over, treat that as an explicit data migration: back up both stores, copy in controlled batches, verify counts and important fields, and only then switch production traffic. LenAPI's `migration`/`migrate` commands initialize MongoDB collections and framework state; they do not copy SQL data. MongoDB document IDs are strings, so update route and response-schema ID types that previously expected integer SQL IDs.
|
|
1690
1880
|
|
|
1691
1881
|
Put application models in `app/models.py` and import that module from `main.py`. The migration command loads `main:app`, so it discovers those model classes before creating a migration. The library's `Model` base supplies an integer `id`:
|
|
1692
1882
|
|
|
@@ -1737,7 +1927,7 @@ student = Student.create(
|
|
|
1737
1927
|
|
|
1738
1928
|
`DateField`, `TimeField`, and `DateTimeField` default to the current UTC date or time when no value or explicit default is supplied. Pass `auto_add_now=False` to disable this behavior, or provide your own `default`. `DurationField(weeks=1)` stores a fixed seven-day `timedelta`; add it to a timestamp when calculating an expiry date. Months and years are calendar intervals and should use calendar-aware date arithmetic.
|
|
1739
1929
|
|
|
1740
|
-
`PositiveIntegerField` and `PositiveBigIntegerField` reject negative values in LenAPI and add a named database `CHECK` constraint, so direct SQL writes are protected too. `EmailField`, `PhoneField`, `IPField`, `URLField`, and `UUIDField` validate values when a model instance is saved. `PhoneField` accepts a
|
|
1930
|
+
`PositiveIntegerField` and `PositiveBigIntegerField` reject negative values in LenAPI and add a named database `CHECK` constraint, so direct SQL writes are protected too. `EmailField`, `PhoneField`, `IPField`, `URLField`, and `UUIDField` validate values when a model instance is saved. `PhoneField` accepts either a `+` country-code number or a local number beginning with `0`, up to 18 characters. Declare phone numbers as strings and send them quoted in JSON so leading zeroes are preserved; bare JSON numbers such as `08099448550` are invalid JSON. `IPField` accepts IPv4 and IPv6. `URLField` accepts `http://`, `https://`, and `www.` URLs. Customize messages with `error_messages`, for example `email = model.EmailField(error_messages={"email": "This is not an email"})`; `unique` is also a supported message key for a unique field.
|
|
1741
1931
|
|
|
1742
1932
|
The base model supplies an auto-incrementing integer `id`. A model may declare its own primary key instead; LenAPI will not add a second `id` key:
|
|
1743
1933
|
|
|
@@ -1801,6 +1991,8 @@ python lenora.py migration --message "add student phone"
|
|
|
1801
1991
|
|
|
1802
1992
|
An existing unversioned SQLite database is adopted only when its schema matches the migration history; adoption preserves its data.
|
|
1803
1993
|
|
|
1994
|
+
`PositiveIntegerField` and `PositiveBigIntegerField` include a named database `CHECK` constraint. LenAPI prevents Alembic's metadata-copy step from adding that constraint twice, which MySQL rejects as a duplicate constraint name. If a local revision generated by an older LenAPI contains duplicate identical `sa.CheckConstraint(...)` entries, correct or regenerate that unapplied revision before using it. MySQL uses non-transactional DDL, so a failed migration may leave some project tables created even though no revision was recorded. Check `showmigrations` and inspect the project-namespaced tables before retrying. For a disposable database, `delete_migrations` can reset this project's tables and revisions after its confirmation prompt; if the data must be preserved, reconcile the partial schema instead of deleting it.
|
|
1995
|
+
|
|
1804
1996
|
`migrate` applies pending migration files already in `migrations/versions/`; it does not generate a revision. A common release workflow is to generate and inspect a revision during development, commit the revision with the code, then run `migrate` during deployment:
|
|
1805
1997
|
|
|
1806
1998
|
```bash
|
|
@@ -1823,9 +2015,9 @@ python lenora.py showmigrations
|
|
|
1823
2015
|
python lenora.py repair-migrations
|
|
1824
2016
|
```
|
|
1825
2017
|
|
|
1826
|
-
`clear_migrations` removes local revision files and
|
|
2018
|
+
`clear_migrations` removes local revision files and this project's Alembic version table only when there are no application tables in this project's namespace. It is for resetting an empty SQL project, not for reconciling an existing schema.
|
|
1827
2019
|
|
|
1828
|
-
`delete_migrations` is a destructive
|
|
2020
|
+
`delete_migrations` is a destructive reset of the current project. After an interactive y/n confirmation, it drops tables and views in this project's SQL namespace (or collections in this project's MongoDB namespace) and deletes local revision files. Objects belonging to other project namespaces are left alone. This also deletes this project's application data. Back up first and use it only when a clean project database is intended:
|
|
1829
2021
|
|
|
1830
2022
|
```bash
|
|
1831
2023
|
python lenora.py delete_migrations
|
|
@@ -1914,7 +2106,14 @@ async_total = async_page.total_count
|
|
|
1914
2106
|
async_pages = async_page.page_count
|
|
1915
2107
|
```
|
|
1916
2108
|
|
|
1917
|
-
`Student.get.all()` returns all rows synchronously; its async counterpart is `await Student.aget.all()`. For filtered
|
|
2109
|
+
`Student.get.all()` returns all rows synchronously; its async counterpart is `await Student.aget.all()`. For filtered lists, use `Student.filter(...).all()` or `await Student.afilter(...).all()`. Both sync and async filtered querysets support existence checks:
|
|
2110
|
+
|
|
2111
|
+
```python
|
|
2112
|
+
has_python_students = Student.filter(course="Python").exists()
|
|
2113
|
+
has_python_students_async = await Student.afilter(course="Python").exists()
|
|
2114
|
+
```
|
|
2115
|
+
|
|
2116
|
+
`Student.get(...)` builds a single-row lookup; call `.get()` for the model instance or `.exists()` for a boolean. The async single-row counterpart is `await Student.aget(...)`, and its existence check is `await Student.aget(...).exists()`. Multiple matches are an error rather than an arbitrary row selection. `Student.get("field", ...).get(...)` selects one or more columns; one selected column returns a scalar and multiple columns return a mapping. This is a query projection, not a uniqueness constraint. To make the database reject duplicates, declare `unique=True` on the field.
|
|
1918
2117
|
|
|
1919
2118
|
### Fetch related records
|
|
1920
2119
|
|