phzyx-forge 0.1.0__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.
- phzyx_forge-0.1.0/PKG-INFO +74 -0
- phzyx_forge-0.1.0/README.md +23 -0
- phzyx_forge-0.1.0/pyproject.toml +67 -0
- phzyx_forge-0.1.0/src/phzyx/admin/__init__.py +153 -0
- phzyx_forge-0.1.0/src/phzyx/admin/actions.py +269 -0
- phzyx_forge-0.1.0/src/phzyx/admin/async_service.py +263 -0
- phzyx_forge-0.1.0/src/phzyx/admin/audit.py +50 -0
- phzyx_forge-0.1.0/src/phzyx/admin/auth.py +69 -0
- phzyx_forge-0.1.0/src/phzyx/admin/branding.py +30 -0
- phzyx_forge-0.1.0/src/phzyx/admin/errors.py +50 -0
- phzyx_forge-0.1.0/src/phzyx/admin/export_copy.py +86 -0
- phzyx_forge-0.1.0/src/phzyx/admin/fastapi/__init__.py +7 -0
- phzyx_forge-0.1.0/src/phzyx/admin/fastapi/board.py +190 -0
- phzyx_forge-0.1.0/src/phzyx/admin/fastapi/routes.py +731 -0
- phzyx_forge-0.1.0/src/phzyx/admin/filters.py +153 -0
- phzyx_forge-0.1.0/src/phzyx/admin/i18n.py +61 -0
- phzyx_forge-0.1.0/src/phzyx/admin/identity.py +63 -0
- phzyx_forge-0.1.0/src/phzyx/admin/layouts.py +45 -0
- phzyx_forge-0.1.0/src/phzyx/admin/litestar/__init__.py +127 -0
- phzyx_forge-0.1.0/src/phzyx/admin/media.py +113 -0
- phzyx_forge-0.1.0/src/phzyx/admin/openapi_export.py +265 -0
- phzyx_forge-0.1.0/src/phzyx/admin/opener.py +35 -0
- phzyx_forge-0.1.0/src/phzyx/admin/relation_manager.py +63 -0
- phzyx_forge-0.1.0/src/phzyx/admin/resource.py +511 -0
- phzyx_forge-0.1.0/src/phzyx/admin/schema_status.py +90 -0
- phzyx_forge-0.1.0/src/phzyx/admin/search.py +77 -0
- phzyx_forge-0.1.0/src/phzyx/admin/security.py +66 -0
- phzyx_forge-0.1.0/src/phzyx/admin/service.py +908 -0
- phzyx_forge-0.1.0/src/phzyx/admin/settings.py +71 -0
- phzyx_forge-0.1.0/src/phzyx/admin/storage.py +158 -0
- phzyx_forge-0.1.0/src/phzyx/admin/storage_cloud.py +353 -0
- phzyx_forge-0.1.0/src/phzyx/admin/stream_export.py +145 -0
- phzyx_forge-0.1.0/src/phzyx/admin/themes.py +50 -0
- phzyx_forge-0.1.0/src/phzyx/admin/unique_checks.py +159 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/apple-touch-icon.png +0 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/assets/index-CRk1fSOo.js +76 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/assets/index-ZF5lh_j6.css +1 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/favicon-64.png +0 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/favicon.png +0 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/index.html +20 -0
- phzyx_forge-0.1.0/src/phzyx/admin/web/dist/phzyx-mark.svg +61 -0
- phzyx_forge-0.1.0/src/phzyx/admin/widgets.py +62 -0
- phzyx_forge-0.1.0/src/phzyx/cli/__init__.py +542 -0
- phzyx_forge-0.1.0/src/phzyx/cli/add_admin.py +43 -0
- phzyx_forge-0.1.0/src/phzyx/cli/add_auth.py +156 -0
- phzyx_forge-0.1.0/src/phzyx/cli/doctor.py +287 -0
- phzyx_forge-0.1.0/src/phzyx/cli/forge_init.py +58 -0
- phzyx_forge-0.1.0/src/phzyx/cli/new.py +108 -0
- phzyx_forge-0.1.0/src/phzyx/cli/presets/__init__.py +21 -0
- phzyx_forge-0.1.0/src/phzyx/cli/presets/full.json +16 -0
- phzyx_forge-0.1.0/src/phzyx/cli/presets/max.json +16 -0
- phzyx_forge-0.1.0/src/phzyx/cli/presets/minimal.json +14 -0
- phzyx_forge-0.1.0/src/phzyx/cli/presets/normal.json +15 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/__init__.py +89 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/detect.py +134 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/plan.py +217 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/presets.py +87 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/prompts.py +37 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/templates.py +791 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/types.py +77 -0
- phzyx_forge-0.1.0/src/phzyx/cli/scaffold/write.py +45 -0
- phzyx_forge-0.1.0/src/phzyx/engine/__init__.py +99 -0
- phzyx_forge-0.1.0/src/phzyx/engine/apply_ops.py +118 -0
- phzyx_forge-0.1.0/src/phzyx/engine/capabilities.py +133 -0
- phzyx_forge-0.1.0/src/phzyx/engine/config.py +45 -0
- phzyx_forge-0.1.0/src/phzyx/engine/connection.py +106 -0
- phzyx_forge-0.1.0/src/phzyx/engine/dialect_spi.py +28 -0
- phzyx_forge-0.1.0/src/phzyx/engine/diff.py +124 -0
- phzyx_forge-0.1.0/src/phzyx/engine/dsn.py +126 -0
- phzyx_forge-0.1.0/src/phzyx/engine/errors.py +19 -0
- phzyx_forge-0.1.0/src/phzyx/engine/introspect_mssql.py +164 -0
- phzyx_forge-0.1.0/src/phzyx/engine/introspect_mysql.py +156 -0
- phzyx_forge-0.1.0/src/phzyx/engine/introspect_postgres.py +176 -0
- phzyx_forge-0.1.0/src/phzyx/engine/introspect_sqlite.py +134 -0
- phzyx_forge-0.1.0/src/phzyx/engine/lint.py +121 -0
- phzyx_forge-0.1.0/src/phzyx/engine/migrate.py +303 -0
- phzyx_forge-0.1.0/src/phzyx/engine/normalize.py +84 -0
- phzyx_forge-0.1.0/src/phzyx/engine/ops.py +57 -0
- phzyx_forge-0.1.0/src/phzyx/engine/plan.py +42 -0
- phzyx_forge-0.1.0/src/phzyx/engine/policy.py +61 -0
- phzyx_forge-0.1.0/src/phzyx/engine/push.py +82 -0
- phzyx_forge-0.1.0/src/phzyx/engine/tui_push.py +85 -0
- phzyx_forge-0.1.0/src/phzyx/orm/__init__.py +130 -0
- phzyx_forge-0.1.0/src/phzyx/orm/bulk.py +131 -0
- phzyx_forge-0.1.0/src/phzyx/orm/engine_async.py +82 -0
- phzyx_forge-0.1.0/src/phzyx/orm/engine_sync.py +132 -0
- phzyx_forge-0.1.0/src/phzyx/orm/errors.py +15 -0
- phzyx_forge-0.1.0/src/phzyx/orm/expr.py +153 -0
- phzyx_forge-0.1.0/src/phzyx/orm/files.py +65 -0
- phzyx_forge-0.1.0/src/phzyx/orm/hydrate.py +31 -0
- phzyx_forge-0.1.0/src/phzyx/orm/insert_prep.py +131 -0
- phzyx_forge-0.1.0/src/phzyx/orm/keyset.py +104 -0
- phzyx_forge-0.1.0/src/phzyx/orm/pk_util.py +42 -0
- phzyx_forge-0.1.0/src/phzyx/orm/query_api.md +60 -0
- phzyx_forge-0.1.0/src/phzyx/orm/relations.py +57 -0
- phzyx_forge-0.1.0/src/phzyx/orm/session_async.py +413 -0
- phzyx_forge-0.1.0/src/phzyx/orm/session_sync.py +481 -0
- phzyx_forge-0.1.0/src/phzyx/orm/soft_delete.py +53 -0
- phzyx_forge-0.1.0/src/phzyx/orm/sql.py +222 -0
- phzyx_forge-0.1.0/src/phzyx/orm/sqlite_params.py +32 -0
- phzyx_forge-0.1.0/src/phzyx/orm/upsert.py +162 -0
- phzyx_forge-0.1.0/src/phzyx/schema/__init__.py +109 -0
- phzyx_forge-0.1.0/src/phzyx/schema/ddl.py +308 -0
- phzyx_forge-0.1.0/src/phzyx/schema/errors.py +7 -0
- phzyx_forge-0.1.0/src/phzyx/schema/field.py +265 -0
- phzyx_forge-0.1.0/src/phzyx/schema/manifest.py +473 -0
- phzyx_forge-0.1.0/src/phzyx/schema/model.py +196 -0
- phzyx_forge-0.1.0/src/phzyx/schema/registry.py +151 -0
- phzyx_forge-0.1.0/src/phzyx/schema/types.py +245 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phzyx-forge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Phzyx Forge — schema, failsafe migrations, ORM, AdminBoard, CLI
|
|
5
|
+
Project-URL: Homepage, https://phzyx.xyz
|
|
6
|
+
Project-URL: Documentation, https://phzyx.xyz/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/phzyxyz/forge
|
|
8
|
+
Project-URL: Issues, https://github.com/phzyxyz/forge/issues
|
|
9
|
+
Author: Athul Nandaswaroop
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: admin,fastapi,migrations,orm,phzyx,pydantic
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: aiosqlite
|
|
25
|
+
Requires-Dist: fastapi>=0.115
|
|
26
|
+
Requires-Dist: pydantic>=2
|
|
27
|
+
Requires-Dist: python-multipart>=0.0.32
|
|
28
|
+
Provides-Extra: auth
|
|
29
|
+
Requires-Dist: phzyx-auth[fastapi]<0.2,>=0.1.0; extra == 'auth'
|
|
30
|
+
Provides-Extra: cache
|
|
31
|
+
Requires-Dist: phzyx-cache<0.2,>=0.1.0; extra == 'cache'
|
|
32
|
+
Provides-Extra: full
|
|
33
|
+
Requires-Dist: asyncpg; extra == 'full'
|
|
34
|
+
Requires-Dist: litestar>=2.12; extra == 'full'
|
|
35
|
+
Requires-Dist: phzyx-auth[fastapi]<0.2,>=0.1.0; extra == 'full'
|
|
36
|
+
Requires-Dist: phzyx-cache<0.2,>=0.1.0; extra == 'full'
|
|
37
|
+
Requires-Dist: psycopg[binary]>=3; extra == 'full'
|
|
38
|
+
Requires-Dist: pymssql>=2.3; extra == 'full'
|
|
39
|
+
Requires-Dist: pymysql; extra == 'full'
|
|
40
|
+
Provides-Extra: litestar
|
|
41
|
+
Requires-Dist: litestar>=2.12; extra == 'litestar'
|
|
42
|
+
Provides-Extra: mssql
|
|
43
|
+
Requires-Dist: pymssql>=2.3; extra == 'mssql'
|
|
44
|
+
Provides-Extra: mysql
|
|
45
|
+
Requires-Dist: pymysql; extra == 'mysql'
|
|
46
|
+
Provides-Extra: postgres
|
|
47
|
+
Requires-Dist: asyncpg; extra == 'postgres'
|
|
48
|
+
Requires-Dist: psycopg[binary]>=3; extra == 'postgres'
|
|
49
|
+
Provides-Extra: sqlite
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# phzyx-forge
|
|
53
|
+
|
|
54
|
+
**Forge** — Phzyx data stack in one install: Pydantic schema, failsafe migrations,
|
|
55
|
+
ORM, AdminBoard (FastAPI-first), and the `phzyx` CLI.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv add phzyx-forge
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from phzyx.schema.model import Model
|
|
63
|
+
from phzyx.orm import Session
|
|
64
|
+
from phzyx.admin.fastapi import AdminBoard
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Optional batteries (separate packages):
|
|
68
|
+
|
|
69
|
+
- `phzyx-auth` — identity / sessions / JWT / RBAC (`uv add 'phzyx-forge[auth]'`)
|
|
70
|
+
- `phzyx-cache` — versioned resource cache
|
|
71
|
+
|
|
72
|
+
Pre-1.0: APIs may change. Pin `==0.1.0` for apps.
|
|
73
|
+
|
|
74
|
+
Homepage: https://phzyx.xyz · Source: https://github.com/phzyxyz/forge
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# phzyx-forge
|
|
2
|
+
|
|
3
|
+
**Forge** — Phzyx data stack in one install: Pydantic schema, failsafe migrations,
|
|
4
|
+
ORM, AdminBoard (FastAPI-first), and the `phzyx` CLI.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
uv add phzyx-forge
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from phzyx.schema.model import Model
|
|
12
|
+
from phzyx.orm import Session
|
|
13
|
+
from phzyx.admin.fastapi import AdminBoard
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Optional batteries (separate packages):
|
|
17
|
+
|
|
18
|
+
- `phzyx-auth` — identity / sessions / JWT / RBAC (`uv add 'phzyx-forge[auth]'`)
|
|
19
|
+
- `phzyx-cache` — versioned resource cache
|
|
20
|
+
|
|
21
|
+
Pre-1.0: APIs may change. Pin `==0.1.0` for apps.
|
|
22
|
+
|
|
23
|
+
Homepage: https://phzyx.xyz · Source: https://github.com/phzyxyz/forge
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "phzyx-forge"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Phzyx Forge — schema, failsafe migrations, ORM, AdminBoard, CLI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Athul Nandaswaroop" }]
|
|
9
|
+
keywords = ["phzyx", "orm", "pydantic", "admin", "migrations", "fastapi"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Framework :: FastAPI",
|
|
20
|
+
"Topic :: Database",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pydantic>=2",
|
|
25
|
+
"fastapi>=0.115",
|
|
26
|
+
"python-multipart>=0.0.32",
|
|
27
|
+
"aiosqlite", # default local stack; postgres/mysql/mssql stay extras
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
# [sqlite] kept as no-op alias so old docs still resolve
|
|
32
|
+
sqlite = []
|
|
33
|
+
postgres = ["asyncpg", "psycopg[binary]>=3"]
|
|
34
|
+
mysql = ["pymysql"]
|
|
35
|
+
mssql = ["pymssql>=2.3"]
|
|
36
|
+
auth = ["phzyx-auth[fastapi]>=0.1.0,<0.2"]
|
|
37
|
+
cache = ["phzyx-cache>=0.1.0,<0.2"]
|
|
38
|
+
litestar = ["litestar>=2.12"]
|
|
39
|
+
# Literally full: every driver + batteries + Litestar (SQLite already in core).
|
|
40
|
+
full = [
|
|
41
|
+
"asyncpg",
|
|
42
|
+
"psycopg[binary]>=3",
|
|
43
|
+
"pymysql",
|
|
44
|
+
"pymssql>=2.3",
|
|
45
|
+
"phzyx-auth[fastapi]>=0.1.0,<0.2",
|
|
46
|
+
"phzyx-cache>=0.1.0,<0.2",
|
|
47
|
+
"litestar>=2.12",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
phzyx = "phzyx.cli:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://phzyx.xyz"
|
|
55
|
+
Documentation = "https://phzyx.xyz/docs"
|
|
56
|
+
Repository = "https://github.com/phzyxyz/forge"
|
|
57
|
+
Issues = "https://github.com/phzyxyz/forge/issues"
|
|
58
|
+
|
|
59
|
+
[build-system]
|
|
60
|
+
requires = ["hatchling"]
|
|
61
|
+
build-backend = "hatchling.build"
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/phzyx"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = ["src/phzyx", "README.md", "pyproject.toml"]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Phzyx AdminBoard core — headless admin service and SPA assets."""
|
|
2
|
+
|
|
3
|
+
from pkgutil import extend_path
|
|
4
|
+
|
|
5
|
+
from phzyx.admin.actions import (
|
|
6
|
+
AdminAction,
|
|
7
|
+
BulkAction,
|
|
8
|
+
list_admin_actions,
|
|
9
|
+
list_bulk_actions,
|
|
10
|
+
run_admin_action,
|
|
11
|
+
run_bulk_action,
|
|
12
|
+
)
|
|
13
|
+
from phzyx.admin.async_service import AsyncAdminService
|
|
14
|
+
from phzyx.admin.audit import AuditEvent, AuditLog
|
|
15
|
+
from phzyx.admin.auth import AllowAll, Authz, DenyWrites, require_authz
|
|
16
|
+
from phzyx.admin.errors import (
|
|
17
|
+
AdminError,
|
|
18
|
+
ConflictError,
|
|
19
|
+
ForbiddenError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
UnauthorizedError,
|
|
22
|
+
ValidationAdminError,
|
|
23
|
+
)
|
|
24
|
+
from phzyx.admin.export_copy import copy_payload, export_csv, export_json
|
|
25
|
+
from phzyx.admin.i18n import DEFAULT_CATALOG, I18n
|
|
26
|
+
from phzyx.admin.identity import decode_identity, encode_identity
|
|
27
|
+
from phzyx.admin.layouts import LayoutPrefs, LayoutStore
|
|
28
|
+
from phzyx.admin.media import MediaItem, MediaLibrary
|
|
29
|
+
from phzyx.admin.opener import OpenMeta, open_meta_for_key
|
|
30
|
+
from phzyx.admin.relation_manager import RelationManager, all_relation_managers
|
|
31
|
+
from phzyx.admin.resource import (
|
|
32
|
+
ColumnConfig,
|
|
33
|
+
FieldsetConfig,
|
|
34
|
+
RelationConfig,
|
|
35
|
+
ResourceConfig,
|
|
36
|
+
infer_resource,
|
|
37
|
+
m2m_relation,
|
|
38
|
+
override_resource,
|
|
39
|
+
wire_relations,
|
|
40
|
+
)
|
|
41
|
+
from phzyx.admin.search import SearchHit, global_search
|
|
42
|
+
from phzyx.admin.security import (
|
|
43
|
+
assert_no_mass_assignment,
|
|
44
|
+
csp_headers,
|
|
45
|
+
csrf_cookie_notes,
|
|
46
|
+
is_production_like,
|
|
47
|
+
)
|
|
48
|
+
from phzyx.admin.service import AdminService, ListResult
|
|
49
|
+
from phzyx.admin.settings import AdminSettings, SettingsStore
|
|
50
|
+
from phzyx.admin.storage import LocalStorage, MemoryStorage, StorageBackend, guess_content_type
|
|
51
|
+
from phzyx.admin.storage_cloud import AzureBlobStorage, GCSStorage, S3CompatibleStorage
|
|
52
|
+
from phzyx.admin.stream_export import (
|
|
53
|
+
BOUNDED_RSS_NOTE,
|
|
54
|
+
DEFAULT_EXPORT_POLICY,
|
|
55
|
+
EXCEL_MAX_ROWS,
|
|
56
|
+
ExportPolicy,
|
|
57
|
+
export_xlsx,
|
|
58
|
+
stream_csv,
|
|
59
|
+
stream_json_array,
|
|
60
|
+
stream_jsonl,
|
|
61
|
+
)
|
|
62
|
+
from phzyx.admin.themes import (
|
|
63
|
+
DEFAULT_THEME,
|
|
64
|
+
PRIMARY_THEMES,
|
|
65
|
+
ThemeName,
|
|
66
|
+
normalize_theme,
|
|
67
|
+
resolve_theme,
|
|
68
|
+
theme_tokens_css,
|
|
69
|
+
)
|
|
70
|
+
from phzyx.admin.widgets import CountWidget, resource_count_widgets
|
|
71
|
+
|
|
72
|
+
__path__ = extend_path(__path__, __name__)
|
|
73
|
+
|
|
74
|
+
__version__ = "0.1.0"
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"BOUNDED_RSS_NOTE",
|
|
78
|
+
"DEFAULT_EXPORT_POLICY",
|
|
79
|
+
"DEFAULT_THEME",
|
|
80
|
+
"EXCEL_MAX_ROWS",
|
|
81
|
+
"PRIMARY_THEMES",
|
|
82
|
+
"AdminAction",
|
|
83
|
+
"AdminError",
|
|
84
|
+
"AdminService",
|
|
85
|
+
"AdminSettings",
|
|
86
|
+
"AsyncAdminService",
|
|
87
|
+
"AllowAll",
|
|
88
|
+
"CountWidget",
|
|
89
|
+
"DEFAULT_CATALOG",
|
|
90
|
+
"I18n",
|
|
91
|
+
"RelationManager",
|
|
92
|
+
"SearchHit",
|
|
93
|
+
"AuditEvent",
|
|
94
|
+
"AuditLog",
|
|
95
|
+
"Authz",
|
|
96
|
+
"AzureBlobStorage",
|
|
97
|
+
"BulkAction",
|
|
98
|
+
"ColumnConfig",
|
|
99
|
+
"ConflictError",
|
|
100
|
+
"DenyWrites",
|
|
101
|
+
"FieldsetConfig",
|
|
102
|
+
"ExportPolicy",
|
|
103
|
+
"ForbiddenError",
|
|
104
|
+
"GCSStorage",
|
|
105
|
+
"LayoutPrefs",
|
|
106
|
+
"LayoutStore",
|
|
107
|
+
"ListResult",
|
|
108
|
+
"LocalStorage",
|
|
109
|
+
"MediaItem",
|
|
110
|
+
"MediaLibrary",
|
|
111
|
+
"MemoryStorage",
|
|
112
|
+
"NotFoundError",
|
|
113
|
+
"OpenMeta",
|
|
114
|
+
"RelationConfig",
|
|
115
|
+
"m2m_relation",
|
|
116
|
+
"ResourceConfig",
|
|
117
|
+
"S3CompatibleStorage",
|
|
118
|
+
"SettingsStore",
|
|
119
|
+
"StorageBackend",
|
|
120
|
+
"ThemeName",
|
|
121
|
+
"UnauthorizedError",
|
|
122
|
+
"ValidationAdminError",
|
|
123
|
+
"__version__",
|
|
124
|
+
"assert_no_mass_assignment",
|
|
125
|
+
"copy_payload",
|
|
126
|
+
"csp_headers",
|
|
127
|
+
"csrf_cookie_notes",
|
|
128
|
+
"decode_identity",
|
|
129
|
+
"encode_identity",
|
|
130
|
+
"all_relation_managers",
|
|
131
|
+
"export_csv",
|
|
132
|
+
"export_json",
|
|
133
|
+
"export_xlsx",
|
|
134
|
+
"global_search",
|
|
135
|
+
"guess_content_type",
|
|
136
|
+
"resource_count_widgets",
|
|
137
|
+
"infer_resource",
|
|
138
|
+
"is_production_like",
|
|
139
|
+
"list_admin_actions",
|
|
140
|
+
"list_bulk_actions",
|
|
141
|
+
"normalize_theme",
|
|
142
|
+
"open_meta_for_key",
|
|
143
|
+
"override_resource",
|
|
144
|
+
"require_authz",
|
|
145
|
+
"resolve_theme",
|
|
146
|
+
"run_admin_action",
|
|
147
|
+
"run_bulk_action",
|
|
148
|
+
"stream_csv",
|
|
149
|
+
"stream_json_array",
|
|
150
|
+
"stream_jsonl",
|
|
151
|
+
"theme_tokens_css",
|
|
152
|
+
"wire_relations",
|
|
153
|
+
]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Admin action registry — row / bulk / global (Diamond D2/D5)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
from phzyx.admin.auth import require_authz
|
|
10
|
+
from phzyx.admin.errors import NotFoundError, ValidationAdminError
|
|
11
|
+
from phzyx.admin.identity import decode_identity
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from phzyx.admin.service import AdminService
|
|
15
|
+
|
|
16
|
+
Scope = Literal["row", "bulk", "global"]
|
|
17
|
+
|
|
18
|
+
# client: SPA-side execution hint (no server mutation body required)
|
|
19
|
+
ClientKind = str | None # export:csv | export:json | export:xlsx | copy:json | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class AdminAction:
|
|
24
|
+
"""Declarative admin action (Diamond D5)."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
label: str
|
|
28
|
+
description: str = ""
|
|
29
|
+
scope: Scope = "bulk"
|
|
30
|
+
confirm: bool = False
|
|
31
|
+
danger: bool = False
|
|
32
|
+
client: ClientKind = None # if set, SPA runs client handler
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Backward-compatible alias
|
|
36
|
+
BulkAction = AdminAction
|
|
37
|
+
|
|
38
|
+
Handler = Callable[["AdminService", str, Sequence[str], Any | None], dict[str, Any]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def action_deactivate(
|
|
42
|
+
service: AdminService,
|
|
43
|
+
resource: str,
|
|
44
|
+
identities: Sequence[str],
|
|
45
|
+
principal: Any | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Set ``active=False`` on each identity when the model has that field."""
|
|
48
|
+
res = service.get_resource(resource)
|
|
49
|
+
if not any(c.name == "active" for c in res.columns):
|
|
50
|
+
raise ValidationAdminError("resource has no active field")
|
|
51
|
+
updated = 0
|
|
52
|
+
for ident in identities:
|
|
53
|
+
key = decode_identity(res.model, ident)
|
|
54
|
+
obj = service.session.get(res.model, key)
|
|
55
|
+
if obj is None:
|
|
56
|
+
continue
|
|
57
|
+
service.session.update(obj, active=False)
|
|
58
|
+
updated += 1
|
|
59
|
+
service.session.commit()
|
|
60
|
+
if service.audit:
|
|
61
|
+
pid = getattr(principal, "id", None) if principal else None
|
|
62
|
+
service.audit.record(
|
|
63
|
+
"bulk_deactivate",
|
|
64
|
+
resource,
|
|
65
|
+
principal_id=pid,
|
|
66
|
+
count=updated,
|
|
67
|
+
)
|
|
68
|
+
return {"action": "deactivate", "updated": updated, "ok": True}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def action_activate(
|
|
72
|
+
service: AdminService,
|
|
73
|
+
resource: str,
|
|
74
|
+
identities: Sequence[str],
|
|
75
|
+
principal: Any | None = None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""Set ``active=True`` on selected identities."""
|
|
78
|
+
res = service.get_resource(resource)
|
|
79
|
+
if not any(c.name == "active" for c in res.columns):
|
|
80
|
+
raise ValidationAdminError("resource has no active field")
|
|
81
|
+
updated = 0
|
|
82
|
+
for ident in identities:
|
|
83
|
+
key = decode_identity(res.model, ident)
|
|
84
|
+
obj = service.session.get(res.model, key)
|
|
85
|
+
if obj is None:
|
|
86
|
+
continue
|
|
87
|
+
service.session.update(obj, active=True)
|
|
88
|
+
updated += 1
|
|
89
|
+
service.session.commit()
|
|
90
|
+
if service.audit:
|
|
91
|
+
pid = getattr(principal, "id", None) if principal else None
|
|
92
|
+
service.audit.record("bulk_activate", resource, principal_id=pid, count=updated)
|
|
93
|
+
return {"action": "activate", "updated": updated, "ok": True}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
DEFAULT_HANDLERS: dict[str, Handler] = {
|
|
97
|
+
"deactivate": action_deactivate,
|
|
98
|
+
"activate": action_activate,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Prefer DEFAULT_HANDLERS; keep alias for D2 imports
|
|
102
|
+
DEFAULT_BULK_HANDLERS = DEFAULT_HANDLERS
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def default_actions(*, has_active: bool) -> list[AdminAction]:
|
|
106
|
+
"""Built-in catalog: server mutations + client export/copy."""
|
|
107
|
+
acts: list[AdminAction] = [
|
|
108
|
+
AdminAction(
|
|
109
|
+
name="export_csv",
|
|
110
|
+
label="Export CSV",
|
|
111
|
+
description="Download resource as CSV",
|
|
112
|
+
scope="global",
|
|
113
|
+
client="export:csv",
|
|
114
|
+
),
|
|
115
|
+
AdminAction(
|
|
116
|
+
name="export_json",
|
|
117
|
+
label="Export JSON",
|
|
118
|
+
description="Download resource as JSON",
|
|
119
|
+
scope="global",
|
|
120
|
+
client="export:json",
|
|
121
|
+
),
|
|
122
|
+
AdminAction(
|
|
123
|
+
name="export_xlsx",
|
|
124
|
+
label="Export Excel",
|
|
125
|
+
description="Download resource as Excel",
|
|
126
|
+
scope="global",
|
|
127
|
+
client="export:xlsx",
|
|
128
|
+
),
|
|
129
|
+
AdminAction(
|
|
130
|
+
name="copy_json",
|
|
131
|
+
label="Copy JSON",
|
|
132
|
+
description="Copy selection or all as JSON",
|
|
133
|
+
scope="bulk",
|
|
134
|
+
client="copy:json",
|
|
135
|
+
confirm=False,
|
|
136
|
+
),
|
|
137
|
+
AdminAction(
|
|
138
|
+
name="copy_row_json",
|
|
139
|
+
label="Copy JSON",
|
|
140
|
+
description="Copy this row as JSON",
|
|
141
|
+
scope="row",
|
|
142
|
+
client="copy:json",
|
|
143
|
+
confirm=False,
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
if has_active:
|
|
147
|
+
acts.extend(
|
|
148
|
+
[
|
|
149
|
+
AdminAction(
|
|
150
|
+
name="deactivate",
|
|
151
|
+
label="Deactivate",
|
|
152
|
+
description="Set active=false on selected rows",
|
|
153
|
+
scope="bulk",
|
|
154
|
+
confirm=True,
|
|
155
|
+
danger=False,
|
|
156
|
+
),
|
|
157
|
+
AdminAction(
|
|
158
|
+
name="activate",
|
|
159
|
+
label="Activate",
|
|
160
|
+
description="Set active=true on selected rows",
|
|
161
|
+
scope="bulk",
|
|
162
|
+
confirm=True,
|
|
163
|
+
),
|
|
164
|
+
AdminAction(
|
|
165
|
+
name="deactivate",
|
|
166
|
+
label="Deactivate",
|
|
167
|
+
description="Set active=false on this row",
|
|
168
|
+
scope="row",
|
|
169
|
+
confirm=True,
|
|
170
|
+
),
|
|
171
|
+
AdminAction(
|
|
172
|
+
name="activate",
|
|
173
|
+
label="Activate",
|
|
174
|
+
description="Set active=true on this row",
|
|
175
|
+
scope="row",
|
|
176
|
+
confirm=True,
|
|
177
|
+
),
|
|
178
|
+
]
|
|
179
|
+
)
|
|
180
|
+
return acts
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
DEFAULT_BULK_ACTIONS: tuple[AdminAction, ...] = tuple(
|
|
184
|
+
a for a in default_actions(has_active=True) if a.scope == "bulk" and not a.client
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _public(a: AdminAction) -> dict[str, Any]:
|
|
189
|
+
return {
|
|
190
|
+
"name": a.name,
|
|
191
|
+
"label": a.label,
|
|
192
|
+
"description": a.description,
|
|
193
|
+
"scope": a.scope,
|
|
194
|
+
"confirm": a.confirm,
|
|
195
|
+
"danger": a.danger,
|
|
196
|
+
"client": a.client,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def list_admin_actions(
|
|
201
|
+
resource_name: str,
|
|
202
|
+
*,
|
|
203
|
+
has_active: bool,
|
|
204
|
+
scope: str | None = None,
|
|
205
|
+
) -> list[dict[str, Any]]:
|
|
206
|
+
"""Public action descriptors for SPA (Diamond D5)."""
|
|
207
|
+
_ = resource_name
|
|
208
|
+
out = [_public(a) for a in default_actions(has_active=has_active)]
|
|
209
|
+
if scope:
|
|
210
|
+
out = [a for a in out if a["scope"] == scope]
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def list_bulk_actions(resource_name: str, has_active: bool) -> list[dict[str, Any]]:
|
|
215
|
+
"""Backward-compatible bulk-only list (D2)."""
|
|
216
|
+
return list_admin_actions(resource_name, has_active=has_active, scope="bulk")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def run_admin_action(
|
|
220
|
+
service: AdminService,
|
|
221
|
+
resource: str,
|
|
222
|
+
action: str,
|
|
223
|
+
identities: Sequence[str] | None = None,
|
|
224
|
+
*,
|
|
225
|
+
principal: Any | None = None,
|
|
226
|
+
handlers: dict[str, Handler] | None = None,
|
|
227
|
+
) -> dict[str, Any]:
|
|
228
|
+
"""Run a server-side action (row or bulk)."""
|
|
229
|
+
require_authz(service.authz, principal, f"action:{action}", resource)
|
|
230
|
+
# Client-only actions should not hit the server runner
|
|
231
|
+
catalog = {
|
|
232
|
+
a.name: a
|
|
233
|
+
for a in default_actions(
|
|
234
|
+
has_active=any(c.name == "active" for c in service.get_resource(resource).columns)
|
|
235
|
+
)
|
|
236
|
+
if a.client is None
|
|
237
|
+
}
|
|
238
|
+
reg_probe = handlers or DEFAULT_HANDLERS
|
|
239
|
+
if action not in catalog and action not in reg_probe and action not in DEFAULT_HANDLERS:
|
|
240
|
+
raise NotFoundError(f"unknown action {action}")
|
|
241
|
+
|
|
242
|
+
idents = list(identities or [])
|
|
243
|
+
if not idents:
|
|
244
|
+
raise ValidationAdminError("identities required")
|
|
245
|
+
reg = handlers or DEFAULT_HANDLERS
|
|
246
|
+
handler = reg.get(action)
|
|
247
|
+
if handler is None:
|
|
248
|
+
raise NotFoundError(f"unknown action {action}")
|
|
249
|
+
return handler(service, resource, idents, principal)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def run_bulk_action(
|
|
253
|
+
service: AdminService,
|
|
254
|
+
resource: str,
|
|
255
|
+
action: str,
|
|
256
|
+
identities: Sequence[str],
|
|
257
|
+
*,
|
|
258
|
+
principal: Any | None = None,
|
|
259
|
+
handlers: dict[str, Handler] | None = None,
|
|
260
|
+
) -> dict[str, Any]:
|
|
261
|
+
"""Backward-compatible bulk runner (D2)."""
|
|
262
|
+
return run_admin_action(
|
|
263
|
+
service,
|
|
264
|
+
resource,
|
|
265
|
+
action,
|
|
266
|
+
identities,
|
|
267
|
+
principal=principal,
|
|
268
|
+
handlers=handlers,
|
|
269
|
+
)
|