lenapi 0.1.2__tar.gz → 0.1.3__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.
Files changed (53) hide show
  1. {lenapi-0.1.2 → lenapi-0.1.3}/PKG-INFO +174 -6
  2. {lenapi-0.1.2 → lenapi-0.1.3}/README.md +170 -4
  3. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/__init__.py +3 -2
  4. lenapi-0.1.3/lenapi/admin.py +1017 -0
  5. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/application.py +237 -40
  6. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/cli.py +338 -89
  7. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/database.py +22 -0
  8. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/docs_guard.py +52 -13
  9. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/models.py +193 -21
  10. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/mongodb.py +16 -2
  11. lenapi-0.1.3/lenapi/project.py +62 -0
  12. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/request.py +8 -3
  13. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/security_store.py +12 -3
  14. lenapi-0.1.3/lenapi/transaction.py +76 -0
  15. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi.egg-info/PKG-INFO +174 -6
  16. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi.egg-info/SOURCES.txt +6 -0
  17. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi.egg-info/requires.txt +3 -0
  18. lenapi-0.1.3/lenapi.egg-info/top_level.txt +2 -0
  19. lenapi-0.1.3/lenapi_bootstrap.py +97 -0
  20. {lenapi-0.1.2 → lenapi-0.1.3}/pyproject.toml +6 -2
  21. lenapi-0.1.3/tests/test_admin_console.py +219 -0
  22. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_framework_features.py +61 -1
  23. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_media_upload.py +20 -0
  24. lenapi-0.1.3/tests/test_project_isolation.py +233 -0
  25. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_user_model_extension.py +7 -0
  26. lenapi-0.1.2/lenapi.egg-info/top_level.txt +0 -1
  27. {lenapi-0.1.2 → lenapi-0.1.3}/LICENSE +0 -0
  28. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/auth.py +0 -0
  29. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/authentication.py +0 -0
  30. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/authorization.py +0 -0
  31. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/background.py +0 -0
  32. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/concurrency.py +0 -0
  33. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/dependencies.py +0 -0
  34. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/logging.py +0 -0
  35. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/middleware.py +0 -0
  36. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/model.py +0 -0
  37. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/openapi.py +0 -0
  38. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/parameters.py +0 -0
  39. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/response.py +0 -0
  40. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/security.py +0 -0
  41. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/sentinels.py +0 -0
  42. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/service.py +0 -0
  43. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi/throttling.py +0 -0
  44. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi.egg-info/dependency_links.txt +0 -0
  45. {lenapi-0.1.2 → lenapi-0.1.3}/lenapi.egg-info/entry_points.txt +0 -0
  46. {lenapi-0.1.2 → lenapi-0.1.3}/setup.cfg +0 -0
  47. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_cli_settings_recovery.py +0 -0
  48. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_docs_guard.py +0 -0
  49. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_mongodb.py +0 -0
  50. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_platform_services.py +0 -0
  51. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_security_patch.py +0 -0
  52. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_sentinels.py +0 -0
  53. {lenapi-0.1.2 → lenapi-0.1.3}/tests/test_throttle.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lenapi
3
- Version: 0.1.2
3
+ Version: 0.1.3
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://github.com/Mikandex/lenapi_documentation
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
@@ -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:
@@ -1625,7 +1752,7 @@ async def billing_webhook(
1625
1752
 
1626
1753
  ## Databases
1627
1754
 
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.
1755
+ `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
1756
 
1630
1757
  ```python
1631
1758
  # settings.py — choose one backend and its URL
@@ -1643,6 +1770,8 @@ DATABASE_URL = f"sqlite:///{BASE_DIR / 'lenapi.db'}"
1643
1770
  # MongoDB: install `python -m pip install 'lenapi[mongodb]'`
1644
1771
  # DATABASE_TYPE = "mongodb"
1645
1772
  # DATABASE_URL = "mongodb://user:password@localhost:27017/app_db"
1773
+ # If the URI has no database path, set this explicitly instead:
1774
+ # MONGODB_DATABASE = "app_db"
1646
1775
 
1647
1776
  ACTIVATE_ASYNC = False
1648
1777
  ```
@@ -1686,7 +1815,44 @@ await student.asave()
1686
1815
  await student.adelete()
1687
1816
  ```
1688
1817
 
1689
- `MongoDatabase.ping()` and `await MongoDatabase.aping()` check connectivity. To select MongoDB, set `DATABASE_TYPE = "mongodb"` and set `DATABASE_URL` to a Mongo URI whose path contains the database name, such as `mongodb://localhost:27017/my_database`. `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/).
1818
+ `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/).
1819
+
1820
+ #### Switching an existing project from SQL to MongoDB
1821
+
1822
+ 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`.
1823
+
1824
+ ```python
1825
+ # settings.py
1826
+ DATABASE_TYPE = "mongodb"
1827
+ DATABASE_URL = "mongodb://127.0.0.1:27017/my_app"
1828
+
1829
+ # app/models.py
1830
+ from lenapi import MongoDocument
1831
+
1832
+ class MongoStudent(MongoDocument):
1833
+ collection_name = "students"
1834
+ username: str
1835
+ course: str
1836
+
1837
+ # main.py
1838
+ from lenapi import LENAPI
1839
+ from pydantic import BaseModel
1840
+ from app.models import MongoStudent
1841
+
1842
+ class StudentCreate(BaseModel):
1843
+ username: str
1844
+ course: str
1845
+
1846
+ app = LENAPI()
1847
+ MongoStudent.bind(app.mongodb)
1848
+ print(app.database_type) # mongodb
1849
+
1850
+ @app.post("/students")
1851
+ def create_student(data: StudentCreate):
1852
+ return MongoStudent.create(**data.model_dump())
1853
+ ```
1854
+
1855
+ 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
1856
 
1691
1857
  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
1858
 
@@ -1801,6 +1967,8 @@ python lenora.py migration --message "add student phone"
1801
1967
 
1802
1968
  An existing unversioned SQLite database is adopted only when its schema matches the migration history; adoption preserves its data.
1803
1969
 
1970
+ `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.
1971
+
1804
1972
  `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
1973
 
1806
1974
  ```bash
@@ -1823,9 +1991,9 @@ python lenora.py showmigrations
1823
1991
  python lenora.py repair-migrations
1824
1992
  ```
1825
1993
 
1826
- `clear_migrations` removes local revision files and the Alembic version table only when there are no application tables. It is for resetting an empty SQL project, not for reconciling an existing schema.
1994
+ `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
1995
 
1828
- `delete_migrations` is a destructive full reset. After an interactive y/n confirmation, it drops every table and view in the selected SQL database (or every collection in MongoDB) and deletes local revision files. This also deletes application data. Back up first and use it only when a clean database is intended:
1996
+ `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
1997
 
1830
1998
  ```bash
1831
1999
  python lenora.py delete_migrations
@@ -8,6 +8,18 @@ LenAPI aims to reduce repetitive code and simplify common integrations, helping
8
8
 
9
9
  **LenAPI — Compact by Design, Powerful by Capability.**
10
10
 
11
+ ## Feature overview
12
+
13
+ - Typed HTTP routes with Pydantic request and response validation, Swagger UI, and ReDoc.
14
+ - Synchronous and asynchronous ORM operations, migrations, transactions, search, aggregates, and pagination for SQLite, MySQL, and PostgreSQL. MongoDB document support is currently in beta.
15
+ - Authentication building blocks for JWT, API keys, HTTP Basic, sessions, opaque bearer tokens, HMAC, OIDC, and social sign-in.
16
+ - 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.
17
+ - Validated public and private file uploads, middleware, throttling, and optional gzip compression.
18
+ - An optional model admin console with explicit model/field allow-lists, per-model permissions, and CPU/RAM/process monitoring.
19
+ - Independent generated projects that can run separately or be mounted under a path in a primary project.
20
+
21
+ See the [LenAPI documentation](https://lenapi.site/) for setup guides and runnable examples.
22
+
11
23
  ## Python versions
12
24
 
13
25
  LenAPI is tested and supported on Python 3.11 through 3.14. Python 3.10 reached
@@ -473,6 +485,8 @@ Run `python lenora.py apikey` inside a project to set `AUTHENTICATION = "api_key
473
485
 
474
486
  `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.
475
487
 
488
+ 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`.
489
+
476
490
  ## Automated tests
477
491
 
478
492
  Pytest is installed with lenApi, so you can run the framework test suite from the repository root:
@@ -799,6 +813,117 @@ The docs gate only handles `/`, `/logout`, the configured Swagger and ReDoc path
799
813
 
800
814
  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.
801
815
 
816
+ ### Built-in model admin and resource monitor
817
+
818
+ 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.
819
+
820
+ ```python
821
+ # settings.py
822
+ ADMIN_ENABLED = True
823
+ ADMIN_URL = "/admin/"
824
+ ADMIN_SESSION_LIFETIME_SECONDS = 3600
825
+ ADMIN_COOKIE_SECURE = None # follows the ASGI HTTPS scheme
826
+
827
+ # Monitoring is optional and requires the admin console to be enabled.
828
+ ADMIN_MONITORING_ENABLED = True
829
+ ```
830
+
831
+ ```bash
832
+ python lenora.py migration
833
+ python lenora.py admin
834
+ python lenora.py run
835
+ ```
836
+
837
+ 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.
838
+
839
+ 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.
840
+
841
+ ```python
842
+ # app/admin.py
843
+ from lenapi.admin import admin
844
+ from .models import Student, Course, Invoice, Staff
845
+
846
+ admin.register(
847
+ Student,
848
+ fields=("person_name", "course", "email", "profile_image"),
849
+ readonly=("email",),
850
+ exclude=("password", "private_notes"),
851
+ )
852
+
853
+ admin.register(
854
+ Course,
855
+ fields=("title", "description", "duration_weeks", "is_active"),
856
+ )
857
+
858
+ admin.register(
859
+ Invoice,
860
+ fields=("invoice_number", "student_id", "amount", "status", "issued_at"),
861
+ readonly=("invoice_number", "issued_at"),
862
+ exclude=("payment_token", "provider_response"),
863
+ )
864
+
865
+ admin.register(
866
+ Staff,
867
+ fields=("name", "work_email", "department"),
868
+ readonly=("work_email",),
869
+ exclude=("password", "salary", "hr_notes"),
870
+ )
871
+ ```
872
+
873
+ 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.
874
+
875
+ 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:
876
+
877
+ ```bash
878
+ python -m pip install 'lenapi[monitoring]'
879
+ ```
880
+
881
+ 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.
882
+
883
+ 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.
884
+
885
+ In generated projects, prefer a relative model import in `app/admin.py` so project names and mounts remain unambiguous:
886
+
887
+ ```python
888
+ from lenapi.admin import admin
889
+ from .models import Student, Course
890
+
891
+ admin.register(Student, fields=("person_name", "course"))
892
+ admin.register(Course, fields=("title", "is_active"))
893
+ ```
894
+
895
+ 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.
896
+
897
+ ### Mount independent projects
898
+
899
+ 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.
900
+
901
+ ```python
902
+ # win/main.py — primary application
903
+ from lenapi import LENAPI
904
+
905
+ app = LENAPI()
906
+ app.mount("/gamer", "games")
907
+ ```
908
+
909
+ The sibling project must be in the same parent directory and have its own generated `settings.py` with a `PROJECT_NAME`:
910
+
911
+ ```text
912
+ workspace/
913
+ ├── win/
914
+ │ ├── main.py
915
+ │ ├── settings.py
916
+ │ └── app/
917
+ └── games/
918
+ ├── main.py
919
+ ├── settings.py
920
+ └── app/
921
+ ```
922
+
923
+ 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.
924
+
925
+ 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.
926
+
802
927
  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/`.
803
928
 
804
929
  Batch request examples:
@@ -1575,7 +1700,7 @@ async def billing_webhook(
1575
1700
 
1576
1701
  ## Databases
1577
1702
 
1578
- `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.
1703
+ `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.
1579
1704
 
1580
1705
  ```python
1581
1706
  # settings.py — choose one backend and its URL
@@ -1593,6 +1718,8 @@ DATABASE_URL = f"sqlite:///{BASE_DIR / 'lenapi.db'}"
1593
1718
  # MongoDB: install `python -m pip install 'lenapi[mongodb]'`
1594
1719
  # DATABASE_TYPE = "mongodb"
1595
1720
  # DATABASE_URL = "mongodb://user:password@localhost:27017/app_db"
1721
+ # If the URI has no database path, set this explicitly instead:
1722
+ # MONGODB_DATABASE = "app_db"
1596
1723
 
1597
1724
  ACTIVATE_ASYNC = False
1598
1725
  ```
@@ -1636,7 +1763,44 @@ await student.asave()
1636
1763
  await student.adelete()
1637
1764
  ```
1638
1765
 
1639
- `MongoDatabase.ping()` and `await MongoDatabase.aping()` check connectivity. To select MongoDB, set `DATABASE_TYPE = "mongodb"` and set `DATABASE_URL` to a Mongo URI whose path contains the database name, such as `mongodb://localhost:27017/my_database`. `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/).
1766
+ `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/).
1767
+
1768
+ #### Switching an existing project from SQL to MongoDB
1769
+
1770
+ 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`.
1771
+
1772
+ ```python
1773
+ # settings.py
1774
+ DATABASE_TYPE = "mongodb"
1775
+ DATABASE_URL = "mongodb://127.0.0.1:27017/my_app"
1776
+
1777
+ # app/models.py
1778
+ from lenapi import MongoDocument
1779
+
1780
+ class MongoStudent(MongoDocument):
1781
+ collection_name = "students"
1782
+ username: str
1783
+ course: str
1784
+
1785
+ # main.py
1786
+ from lenapi import LENAPI
1787
+ from pydantic import BaseModel
1788
+ from app.models import MongoStudent
1789
+
1790
+ class StudentCreate(BaseModel):
1791
+ username: str
1792
+ course: str
1793
+
1794
+ app = LENAPI()
1795
+ MongoStudent.bind(app.mongodb)
1796
+ print(app.database_type) # mongodb
1797
+
1798
+ @app.post("/students")
1799
+ def create_student(data: StudentCreate):
1800
+ return MongoStudent.create(**data.model_dump())
1801
+ ```
1802
+
1803
+ 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.
1640
1804
 
1641
1805
  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`:
1642
1806
 
@@ -1751,6 +1915,8 @@ python lenora.py migration --message "add student phone"
1751
1915
 
1752
1916
  An existing unversioned SQLite database is adopted only when its schema matches the migration history; adoption preserves its data.
1753
1917
 
1918
+ `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.
1919
+
1754
1920
  `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:
1755
1921
 
1756
1922
  ```bash
@@ -1773,9 +1939,9 @@ python lenora.py showmigrations
1773
1939
  python lenora.py repair-migrations
1774
1940
  ```
1775
1941
 
1776
- `clear_migrations` removes local revision files and the Alembic version table only when there are no application tables. It is for resetting an empty SQL project, not for reconciling an existing schema.
1942
+ `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.
1777
1943
 
1778
- `delete_migrations` is a destructive full reset. After an interactive y/n confirmation, it drops every table and view in the selected SQL database (or every collection in MongoDB) and deletes local revision files. This also deletes application data. Back up first and use it only when a clean database is intended:
1944
+ `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:
1779
1945
 
1780
1946
  ```bash
1781
1947
  python lenora.py delete_migrations
@@ -8,7 +8,7 @@ if sys.version_info < (3, 11):
8
8
  "on 2026-10-01 and is no longer supported."
9
9
  )
10
10
 
11
- __version__ = "0.1.2"
11
+ __version__ = "0.1.3"
12
12
 
13
13
  from .application import Application, LENAPI, Router
14
14
  from .background import BackgroundQueue, BackgroundTasks
@@ -17,6 +17,7 @@ from .authentication import authentication
17
17
  from .auth import SocialAuth
18
18
  from .authorization import require_owner, require_permission, require_role
19
19
  from .database import AsyncDatabase, Database
20
+ from .transaction import transaction
20
21
  from .mongodb import MongoDatabase, MongoDocument, MultipleDocumentsFound
21
22
  from .parameters import Body, File
22
23
  from .logging import get_logger, logger
@@ -95,7 +96,7 @@ from .security import (
95
96
  from . import model
96
97
 
97
98
  __all__ = [
98
- "Application", "LENAPI", "Router", "Depends", "BackgroundTasks", "BackgroundQueue", "Cache", "Faker", "OpenAI", "Ollama", "CORSMiddleware", "DocsAuthMiddleware", "GZipMiddleware", "RequestIDMiddleware", "TimingMiddleware", "require_role", "require_permission", "require_owner", "Database", "AsyncDatabase", "MongoDatabase", "MongoDocument", "MultipleDocumentsFound", "Body", "File", "Model", "AdminUser", "AutoField", "logger", "get_logger", "WebSocket", "WebSocketConnection", "WebSocketDisconnect", "WebSocketService",
99
+ "Application", "LENAPI", "Router", "Depends", "BackgroundTasks", "BackgroundQueue", "Cache", "Faker", "OpenAI", "Ollama", "CORSMiddleware", "DocsAuthMiddleware", "GZipMiddleware", "RequestIDMiddleware", "TimingMiddleware", "require_role", "require_permission", "require_owner", "Database", "AsyncDatabase", "transaction", "MongoDatabase", "MongoDocument", "MultipleDocumentsFound", "Body", "File", "Model", "AdminUser", "AutoField", "logger", "get_logger", "WebSocket", "WebSocketConnection", "WebSocketDisconnect", "WebSocketService",
99
100
  "IntegerField", "PositiveIntegerField", "BigIntField", "BigIntegerField", "PositiveBigIntegerField", "DecimalField", "StringField", "EmailField", "IPField", "PhoneField", "TextField", "UUIDField", "ImageField", "FileField", "FileImageField", "URLField", "BooleanField", "FloatField", "User",
100
101
  "DateTimeField", "DateField", "TimeField", "DurationField", "JSONField", "ForeignKeyField", "OneToOneField", "ManyToManyField", "Count", "Sum", "Min", "Max", "Avg", "Request", "UploadFile", "Response", "JSONResponse",
101
102
  "HTTPException", "JWTAuth", "APIKeyAuth", "BasicAuth", "CSRFProtect", "SessionAuth", "OpaqueBearerAuth", "HMACAuth", "OIDCAuth", "WebhookAuth", "hmac_request_signature", "session_auth", "opaque_bearer_auth", "hmac_auth", "oidc_auth", "webhook_auth", "create_authenticator", "authentication", "SocialAuth", "hash_password", "verify_password", "make_password", "check_password", "make_api_key", "hash_api_key", "UNSET", "model", "CASCADE", "SET_NULL", "PROTECT", "cascade", "null", "protected",