half-orm-gen 1.0.0a1__py3-none-any.whl

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.
@@ -0,0 +1,264 @@
1
+ """
2
+ FastAPI template strings for the generated api/app.py.
3
+
4
+ Used by `half_orm litestar generate --fastapi`.
5
+ Only auto-CRUD routes (CRUD_ACCESS) are supported; @api_* decorators are
6
+ Litestar-specific and are ignored in FastAPI mode.
7
+ """
8
+
9
+ from half_orm_gen.templates import CRUD_HELPERS, CRUD_MODULE_IMPORT
10
+
11
+ FRAMEWORK = 'fastapi'
12
+
13
+ HEADER = """\
14
+ # This file is generated by `half_orm litestar generate --fastapi`. Do not edit.
15
+ from typing import Optional, List, Any
16
+ import typing
17
+ import datetime
18
+ import uuid
19
+ import os
20
+ import sys
21
+ from contextlib import asynccontextmanager
22
+
23
+ cur_dir = os.path.dirname(os.path.abspath(__file__))
24
+ sys.path.insert(0, cur_dir)
25
+ par_dir = os.path.join(cur_dir, os.path.pardir)
26
+ sys.path.insert(0, par_dir)
27
+
28
+ from fastapi import FastAPI, APIRouter, HTTPException, Request
29
+ from pydantic import BaseModel
30
+
31
+ from {module} import ho_baseclasses
32
+ from {module} import ho_typeddicts
33
+ from {module} import MODEL
34
+
35
+ try:
36
+ from api.custom.routes import router as _custom_router
37
+ _has_custom = True
38
+ except ImportError:
39
+ _has_custom = False
40
+
41
+ router = APIRouter()
42
+
43
+ """
44
+
45
+ FOOTER = '''
46
+ _HO_WARN = """
47
+ ======================================================================
48
+ halfORM DEV HELPERS ACTIVE — NOT FOR PRODUCTION
49
+ ======================================================================
50
+ /ho_roles : exposes all declared roles (no authentication)
51
+ /ho_access : exposes the full access map filtered by role
52
+ _get_roles : bearer token used directly as a role name
53
+ (no signature verification)
54
+ ho_dev : super-role with full access to all resources
55
+ (Authorization: Bearer ho_dev)
56
+
57
+ Replace the Authorization middleware with a real JWT implementation
58
+ before deploying to production.
59
+ ======================================================================
60
+ """
61
+
62
+ _HO_WARN_SHOWN = False
63
+
64
+ @asynccontextmanager
65
+ async def _ho_lifespan(app: FastAPI):
66
+ global _HO_WARN_SHOWN
67
+ import sys
68
+ await MODEL.aconnect()
69
+ if MODEL._production_mode:
70
+ raise RuntimeError(
71
+ "halfORM DEV HELPERS are active (ho_roles, ho_access, _get_roles fallback). "
72
+ "These routes and the bearer-token-as-role fallback are not safe for production. "
73
+ "Secure or remove them before deploying."
74
+ )
75
+ if not _HO_WARN_SHOWN:
76
+ print(_HO_WARN, file=sys.stderr, flush=True)
77
+ _HO_WARN_SHOWN = True
78
+ yield
79
+
80
+ application = FastAPI(lifespan=_ho_lifespan{openapi_config})
81
+ application.include_router(router)
82
+ if _has_custom:
83
+ application.include_router(_custom_router)
84
+ '''
85
+
86
+ OPENAPI_CONFIG = """, title="{title}", version="{version}" """
87
+
88
+ HO_ACCESS_ROUTE = (
89
+ '\n_STATIC_ACCESS_MAP = {json_str}\n'
90
+ '\n_ACCESS_MAP = get_access_map()\n\n'
91
+ '@router.get("{version_prefix}/ho_access")\n'
92
+ 'async def _crud_access_map(request: Request) -> dict:\n'
93
+ ' authorized_roles = _get_roles(request)\n'
94
+ ' return _filter_access_for_roles(_ACCESS_MAP, authorized_roles)\n'
95
+ )
96
+
97
+ HO_ROLES_ROUTE = (
98
+ '\n_ROLES = {roles_json}\n\n'
99
+ '@router.get("{version_prefix}/ho_roles")\n'
100
+ 'async def _crud_roles_list() -> list:\n'
101
+ ' return _ROLES\n'
102
+ )
103
+
104
+ WS_HELPERS = """
105
+ import json as _json
106
+ from fastapi import WebSocket as _WS, WebSocketDisconnect as _WSD
107
+
108
+ class _ConnectionManager:
109
+ def __init__(self):
110
+ self._sockets: set = set()
111
+
112
+ async def connect(self, ws: _WS) -> None:
113
+ await ws.accept()
114
+ self._sockets.add(ws)
115
+
116
+ def disconnect(self, ws: _WS) -> None:
117
+ self._sockets.discard(ws)
118
+
119
+ async def broadcast(self, message: dict) -> None:
120
+ dead = set()
121
+ for s in set(self._sockets):
122
+ try:
123
+ await s.send_text(_json.dumps(message, default=str))
124
+ except Exception:
125
+ dead.add(s)
126
+ self._sockets -= dead
127
+
128
+ _manager = _ConnectionManager()
129
+
130
+ @router.websocket("{version_prefix}/ws")
131
+ async def _ws_handler(ws: _WS) -> None:
132
+ await _manager.connect(ws)
133
+ try:
134
+ while True:
135
+ await ws.receive_text()
136
+ except _WSD:
137
+ pass
138
+ finally:
139
+ _manager.disconnect(ws)
140
+ """
141
+
142
+ CRUD_GET_LIST = """
143
+ @router.get("{path}", description="{access_description}")
144
+ async def {handler_name}(
145
+ request: Request,
146
+ {filter_params} fields: Optional[List[str]] = None,
147
+ limit: Optional[int] = None,
148
+ offset: Optional[int] = None,
149
+ ) -> list:
150
+ api_excluded = getattr({module_alias}, 'API_EXCLUDED_FIELDS', [])
151
+ roles = _get_roles(request)
152
+ filter_kwargs = {{{filter_dict}}}
153
+ role_filter = _get_role_filter(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "GET", roles)
154
+ authorized = _effective_out_fields(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "GET", roles, api_excluded)
155
+ if fields:
156
+ projection = [f for f in fields if not authorized or f in authorized]
157
+ else:
158
+ projection = authorized
159
+ return await {module_alias}.{class_name}(**{{**filter_kwargs, **role_filter}}).ho_aselect(
160
+ *projection, limit=limit, offset=offset
161
+ )
162
+ """
163
+
164
+ CRUD_GET_ONE = """
165
+ @router.get("{path}/{{id}}", description="{access_description}")
166
+ async def {handler_name}_get(
167
+ request: Request,
168
+ id: {pk_py_type},
169
+ ) -> dict:
170
+ api_excluded = getattr({module_alias}, 'API_EXCLUDED_FIELDS', [])
171
+ roles = _get_roles(request)
172
+ role_filter = _get_role_filter(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "GET", roles)
173
+ authorized = _effective_out_fields(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "GET", roles, api_excluded)
174
+ rows = await {module_alias}.{class_name}({pk_instance_filter}, **role_filter).ho_aselect(*authorized)
175
+ if not rows:
176
+ raise HTTPException(status_code=404)
177
+ return rows[0]
178
+ """
179
+
180
+ CRUD_POST = """
181
+ @router.post("{path}", description="{access_description}")
182
+ async def {handler_name}_create(
183
+ request: Request,
184
+ data: {in_typedict},
185
+ ) -> dict:
186
+ api_excluded = getattr({module_alias}, 'API_EXCLUDED_FIELDS', [])
187
+ in_fields = _effective_in_fields(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "POST", _get_roles(request), api_excluded)
188
+ payload = {{k: v for k, v in data.model_dump(exclude_none=True).items() if not in_fields or k in in_fields}}
189
+ result = await {module_alias}.{class_name}(**payload).ho_ainsert()
190
+ await _manager.broadcast({{"event": "create", "resource": "{resource}", "id": {pk_broadcast_expr}}})
191
+ return result
192
+ """
193
+
194
+ CRUD_PUT = """
195
+ @router.put("{path}/{{id}}", description="{access_description}")
196
+ async def {handler_name}_update(
197
+ request: Request,
198
+ id: {pk_py_type},
199
+ data: {in_typedict},
200
+ ) -> dict:
201
+ api_excluded = getattr({module_alias}, 'API_EXCLUDED_FIELDS', [])
202
+ in_fields = _effective_in_fields(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "PUT", _get_roles(request), api_excluded)
203
+ payload = {{k: v for k, v in data.model_dump(exclude_none=True).items() if not in_fields or k in in_fields}}
204
+ authorized = _effective_out_fields(getattr({module_alias}, 'CRUD_ACCESS', {{}}), "PUT", _get_roles(request), api_excluded)
205
+ result = await {module_alias}.{class_name}({pk_instance_filter}).ho_aupdate(*(authorized or ['*']), **payload)
206
+ if not result:
207
+ raise HTTPException(status_code=404)
208
+ await _manager.broadcast({{"event": "update", "resource": "{resource}", "id": id}})
209
+ return result[0]
210
+ """
211
+
212
+ CRUD_DELETE = """
213
+ @router.delete("{path}/{{id}}", description="{access_description}")
214
+ async def {handler_name}_delete(
215
+ request: Request,
216
+ id: {pk_py_type},
217
+ ) -> None:
218
+ await _ws_broadcast_cascade(
219
+ {module_alias}.{class_name}({pk_instance_filter}), "{resource}", id
220
+ )
221
+ result = await {module_alias}.{class_name}({pk_instance_filter}).ho_adelete('*')
222
+ if not result:
223
+ raise HTTPException(status_code=404)
224
+ await _manager.broadcast({{"event": "delete", "resource": "{resource}", "id": id}})
225
+ """
226
+
227
+ WS_CASCADE_HELPER = """
228
+ _WS_RMAP: dict = {{
229
+ {resource_entries}
230
+ }}
231
+
232
+ async def _ws_broadcast_cascade(inst, resource: str, pk_val, _seen: set | None = None) -> None:
233
+ if _seen is None:
234
+ _seen = set()
235
+ _key = (resource, str(pk_val))
236
+ if _key in _seen:
237
+ return
238
+ _seen.add(_key)
239
+ for _fk in inst._ho_fkeys.values():
240
+ if not _fk.is_reverse or len(_fk.fk_names) != 1:
241
+ continue
242
+ _fk_field = _fk.fk_names[0]
243
+ _fqtn = _fk.remote['fqtn']
244
+ _r = f"{{_fqtn[0].replace('.', '_')}}/{{_fqtn[1]}}"
245
+ if _r not in _WS_RMAP:
246
+ continue
247
+ _cls, _pk = _WS_RMAP[_r]
248
+ for _row in await _cls(**{{_fk_field: pk_val}}).ho_aselect(_pk):
249
+ _rid = _row[_pk]
250
+ await _ws_broadcast_cascade(_cls(**{{_pk: _rid}}), _r, _rid, _seen)
251
+ await _manager.broadcast({{"event": "delete", "resource": _r, "id": _rid}})
252
+ """
253
+
254
+
255
+ def typedict_block(class_name: str, field_names: list, all_fields: dict) -> str:
256
+ from half_orm_gen.crud_routes import _py_type_str
257
+ lines = [f'class {class_name}(BaseModel):']
258
+ valid = [(f, all_fields[f]) for f in field_names if f in all_fields]
259
+ if not valid:
260
+ lines.append(' pass')
261
+ else:
262
+ for fname, fobj in valid:
263
+ lines.append(f' {fname}: Optional[{_py_type_str(fobj.py_type)}] = None')
264
+ return '\n'.join(lines)
half_orm_gen/tools.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ Decorators for exposing halfORM class methods as Litestar API routes.
3
+
4
+ Usage in a halfORM relation class::
5
+
6
+ from half_orm_gen import tools
7
+
8
+ class MyRelation(MODEL.get_relation_class('schema.table')):
9
+ @tools.api_get('/items/{id: uuid}', guards=['connected'])
10
+ async def get_item(self, request: "Request"):
11
+ ...
12
+
13
+ @tools.api_post('/items', guards=['connected'])
14
+ async def create_item(self, request: "Request"):
15
+ ...
16
+
17
+ The decorated methods are discovered by ``half_orm litestar generate`` which
18
+ produces the ``api/main.py`` Litestar application file.
19
+ """
20
+
21
+ import inspect
22
+ from functools import wraps
23
+ from typing import Callable
24
+ from litestar import get, post, put, delete, patch
25
+
26
+
27
+ def create_api_decorator(http_method: str, litestar_decorator: Callable):
28
+ """Create an api_* decorator that mirrors the signature of the given Litestar decorator."""
29
+ litestar_sig = inspect.signature(litestar_decorator)
30
+
31
+ def api_decorator(*args, **kwargs):
32
+ bound_args = litestar_sig.bind(*args, **kwargs)
33
+ bound_args.apply_defaults()
34
+
35
+ def wrapper(func: Callable) -> Callable:
36
+ @wraps(func)
37
+ def inner(*func_args, **func_kwargs):
38
+ return func(*func_args, **func_kwargs)
39
+
40
+ inner.is_api_route = True
41
+ inner.http_method = http_method
42
+ inner.litestar_params = dict(bound_args.arguments)
43
+ inner.metadata = {
44
+ 'signature': inspect.signature(func),
45
+ 'documentation': func.__doc__ or '',
46
+ 'litestar_decorator': litestar_decorator,
47
+ 'bound_arguments': bound_args,
48
+ }
49
+ return inner
50
+
51
+ return wrapper
52
+
53
+ api_decorator.__signature__ = litestar_sig
54
+ api_decorator.__name__ = f"api_{http_method.lower()}"
55
+ api_decorator.__doc__ = (
56
+ f"API decorator for {http_method} routes. "
57
+ f"Accepts the same arguments as litestar.{http_method.lower()}."
58
+ )
59
+ return api_decorator
60
+
61
+
62
+ api_get = create_api_decorator('GET', get)
63
+ api_post = create_api_decorator('POST', post)
64
+ api_put = create_api_decorator('PUT', put)
65
+ api_delete = create_api_decorator('DELETE', delete)
66
+ api_patch = create_api_decorator('PATCH', patch)
@@ -0,0 +1 @@
1
+ 1.0.0-a1
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: half_orm_gen
3
+ Version: 1.0.0a1
4
+ Summary: API and frontend backoffice generation for halfORM projects.
5
+ Author-email: Joël Maïzi <joel.maizi@collorg.org>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/half-orm/half-orm-gen
8
+ Keywords: litestar,half-orm,rest,api,postgresql,code-generation,asgi,orm,svelte,angular
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ License-File: AUTHORS
22
+ Requires-Dist: half-orm<1.1.0,>=1.0.0rc13
23
+ Requires-Dist: half-orm-dev<1.1.0,>=1.0.0a32
24
+ Requires-Dist: litestar>=2.0.0
25
+ Dynamic: license-file
26
+
27
+ # half-orm-gen
28
+
29
+ A [halfORM](https://github.com/half-orm/half-orm) extension that generates a
30
+ [Litestar](https://litestar.dev) or [FastAPI](https://fastapi.tiangolo.com) REST API
31
+ **and** a frontend backoffice ([SvelteKit 5](https://svelte.dev) or
32
+ [Angular](https://angular.dev)) from your halfORM project.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install half-orm-gen
38
+ ```
39
+
40
+ ---
41
+
42
+ ## API
43
+
44
+ ```bash
45
+ # Litestar
46
+ half_orm gen api --litestar
47
+ litestar --app api.app:application run --reload
48
+
49
+ # FastAPI
50
+ half_orm gen api --fastapi
51
+ uvicorn api.app:application --reload
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Frontend backoffice
57
+
58
+ ```bash
59
+ # SvelteKit 5
60
+ half_orm gen frontend --svelte
61
+ cd frontend/svelte && npm install && npm run dev
62
+
63
+ # Angular
64
+ half_orm gen frontend --angular
65
+ cd frontend/angular && npm install && npm start
66
+ ```
67
+
68
+ ---
69
+
70
+ ## See also
71
+
72
+ - [half-orm](https://github.com/half-orm/half-orm) — the PostgreSQL ORM at the core
73
+ - [half-orm-dev](https://github.com/half-orm/half-orm-dev) — the development framework
@@ -0,0 +1,29 @@
1
+ half_orm_gen/__init__.py,sha256=gsIzNnl38-e4XqYc4ud04wZTvebMnzfHer8K5ISvVi0,448
2
+ half_orm_gen/api_routes.py,sha256=wm90JWm5U2lTabfIFXXBKkIFvutjMb6I6qOkanNgdk4,7954
3
+ half_orm_gen/cli_extension.py,sha256=BNALyL3aR3jkPDB9OWIvc5BOUSBnSkYul9tcV7ParXQ,6266
4
+ half_orm_gen/crud_routes.py,sha256=GwF7mTXSeXjmZ7TxDntMAn0uEzCStiK3MSHKhssP1W8,20641
5
+ half_orm_gen/generate.py,sha256=gPlfFuUIbwh47QHadehkbTNYe0f-F02i5y4oI5aoaKM,4094
6
+ half_orm_gen/scaffold.py,sha256=iVRk81YoylJeeQJ9yz56KpzXXqTkJIYrGzC_LLeMCgM,1303
7
+ half_orm_gen/templates.py,sha256=Zdfi_SDnZVOzhiTM7myoDO5bhbpyeaxUcVfkWimr_Os,15542
8
+ half_orm_gen/templates_fastapi.py,sha256=yYauOFha1ehsQyaKbnnnS-wEHo5l59uOmQj1OqNQ6g0,9172
9
+ half_orm_gen/tools.py,sha256=0e557C-rifjurzlOyD2B8UHO7eMrAkaGXfYFCpow5OY,2278
10
+ half_orm_gen/version.txt,sha256=V7I2VeuW_SCn04qwchb5-EAwEtzMEiDRAlYord5iGng,9
11
+ half_orm_gen/gen_app/__init__.py,sha256=OW0tcuXBQUdHwuDBVj7Bo0Xse8U4f3tgukBfhgxsZsM,730
12
+ half_orm_gen/gen_app/angular.py,sha256=6FLSBV9CJV5a-l1tcngANkzKt6-yxECHtjPAmfLJejs,66718
13
+ half_orm_gen/gen_app/svelte.py,sha256=upqduNPimoPfbsRw66H0V93fG8N-lilvvb3jIuGhrj0,50823
14
+ half_orm_gen/gen_store/__init__.py,sha256=Ww3v9nuZNUI-GR5FyzmjSrokRydDt-NhNmKoR11Zd24,844
15
+ half_orm_gen/gen_store/base.py,sha256=mBkoF8X4pEDv2n3ol9-Nqyu3LVGvgOEGDZej_Qv9gbU,3390
16
+ half_orm_gen/gen_store/svelte.py,sha256=G-qIqjEkAbAO3reZL6J-bIK_OEqUwmhf6f64BwWB3_8,12207
17
+ half_orm_gen/scaffolding/api_init.py,sha256=VLdSX8mfJ3P9ul_XOw50g0TzapwfFtErpVIus8ET4pA,13
18
+ half_orm_gen/scaffolding/custom_authorization.py,sha256=3V7BtO5EBE0RxVZ7hUpzyLOAEcNhmWNUZuZfRIQykxg,1435
19
+ half_orm_gen/scaffolding/custom_init.py,sha256=mzymXvpfT4uM2G226Cc48FDa2dY0zbvNO1KC4QwVwUw,20
20
+ half_orm_gen/scaffolding/custom_middlewares_init.py,sha256=oWxx6F34oVC7Hj7qGIBxy3QQcZl7hrGRMIItMeq-XzY,395
21
+ half_orm_gen/scaffolding/custom_routes.py,sha256=YomUbbssjGKhD5wgQoUvW8fEkZcY1LyLa7eH3SEH-PA,362
22
+ half_orm_gen/scaffolding/guards.py,sha256=Z48COmmfXcBDcL4AgwUdofuuXvj8oud5Qi5ZYRTnbw8,1257
23
+ half_orm_gen/scaffolding/roles_core.py,sha256=V6mO33e_DXaCBK4964B4nr3XUfAj7WtDjF4we1k3Wyk,2048
24
+ half_orm_gen-1.0.0a1.dist-info/licenses/AUTHORS,sha256=EaXsDPeylv3aZMFUOd5OJAqS3ZNCIKlj5nTQcjaSZ48,108
25
+ half_orm_gen-1.0.0a1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
26
+ half_orm_gen-1.0.0a1.dist-info/METADATA,sha256=oxg1N98PuYqoTLw-ZFM6axODhkOqG3WSJR6eHbcvDB4,2036
27
+ half_orm_gen-1.0.0a1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
+ half_orm_gen-1.0.0a1.dist-info/top_level.txt,sha256=bF9PTTtutOHtVCTiw62Ih0hqk4M_Ze9smNXdh32i4O0,13
29
+ half_orm_gen-1.0.0a1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ # This is the list of half_orm's significant contributors.
2
+ # Hopefully, it will become larger...
3
+ Joël Maizi