supython 0.5.0__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.
- supython/__init__.py +8 -0
- supython/admin/__init__.py +3 -0
- supython/admin/api/__init__.py +24 -0
- supython/admin/api/auth.py +118 -0
- supython/admin/api/auth_templates.py +67 -0
- supython/admin/api/auth_users.py +225 -0
- supython/admin/api/db.py +174 -0
- supython/admin/api/functions.py +92 -0
- supython/admin/api/jobs.py +192 -0
- supython/admin/api/ops.py +224 -0
- supython/admin/api/realtime.py +281 -0
- supython/admin/api/service_auth.py +49 -0
- supython/admin/api/service_auth_templates.py +83 -0
- supython/admin/api/service_auth_users.py +346 -0
- supython/admin/api/service_db.py +214 -0
- supython/admin/api/service_functions.py +287 -0
- supython/admin/api/service_jobs.py +282 -0
- supython/admin/api/service_ops.py +213 -0
- supython/admin/api/service_realtime.py +30 -0
- supython/admin/api/service_storage.py +220 -0
- supython/admin/api/storage.py +117 -0
- supython/admin/api/system.py +37 -0
- supython/admin/audit.py +29 -0
- supython/admin/deps.py +22 -0
- supython/admin/errors.py +16 -0
- supython/admin/schemas.py +310 -0
- supython/admin/session.py +52 -0
- supython/admin/spa.py +38 -0
- supython/admin/static/assets/Alert-dluGVkos.js +49 -0
- supython/admin/static/assets/Audit-Njung3HI.js +2 -0
- supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
- supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
- supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
- supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
- supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
- supython/admin/static/assets/Crons-B67vc39F.js +2 -0
- supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
- supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
- supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
- supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
- supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
- supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
- supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
- supython/admin/static/assets/Input-DppYTq9C.js +259 -0
- supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
- supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
- supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
- supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
- supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
- supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
- supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
- supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
- supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
- supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
- supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
- supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
- supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
- supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
- supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
- supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
- supython/admin/static/assets/Space-n5-XcguU.js +400 -0
- supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
- supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
- supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
- supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
- supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
- supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
- supython/admin/static/assets/Users-wzwajhlh.js +2 -0
- supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
- supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
- supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
- supython/admin/static/assets/get-Ca6unauB.js +2 -0
- supython/admin/static/assets/index-CeE6v959.js +951 -0
- supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
- supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
- supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
- supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
- supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
- supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
- supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
- supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
- supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
- supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
- supython/admin/static/favicon.svg +1 -0
- supython/admin/static/icons.svg +24 -0
- supython/admin/static/index.html +24 -0
- supython/app.py +149 -0
- supython/auth/__init__.py +3 -0
- supython/auth/_email_job.py +11 -0
- supython/auth/providers/__init__.py +34 -0
- supython/auth/providers/github.py +22 -0
- supython/auth/providers/google.py +19 -0
- supython/auth/providers/oauth.py +56 -0
- supython/auth/providers/registry.py +16 -0
- supython/auth/ratelimit.py +39 -0
- supython/auth/router.py +282 -0
- supython/auth/schemas.py +79 -0
- supython/auth/service.py +587 -0
- supython/body_size.py +184 -0
- supython/cli.py +1653 -0
- supython/client/__init__.py +67 -0
- supython/client/_auth.py +249 -0
- supython/client/_client.py +145 -0
- supython/client/_config.py +92 -0
- supython/client/_functions.py +69 -0
- supython/client/_storage.py +255 -0
- supython/client/py.typed +0 -0
- supython/db.py +151 -0
- supython/db_admin.py +8 -0
- supython/functions/__init__.py +19 -0
- supython/functions/context.py +262 -0
- supython/functions/loader.py +307 -0
- supython/functions/router.py +228 -0
- supython/functions/schemas.py +50 -0
- supython/gen/__init__.py +5 -0
- supython/gen/_introspect.py +137 -0
- supython/gen/types_py.py +270 -0
- supython/gen/types_ts.py +365 -0
- supython/health.py +229 -0
- supython/hooks.py +117 -0
- supython/jobs/__init__.py +31 -0
- supython/jobs/backends.py +97 -0
- supython/jobs/context.py +58 -0
- supython/jobs/cron.py +152 -0
- supython/jobs/cron_inproc.py +118 -0
- supython/jobs/decorators.py +76 -0
- supython/jobs/registry.py +79 -0
- supython/jobs/router.py +136 -0
- supython/jobs/schemas.py +92 -0
- supython/jobs/service.py +311 -0
- supython/jobs/worker.py +219 -0
- supython/jwks.py +257 -0
- supython/keyset.py +279 -0
- supython/logging_config.py +291 -0
- supython/mail.py +33 -0
- supython/mailer.py +65 -0
- supython/migrate.py +81 -0
- supython/migrations/0001_extensions_and_roles.sql +46 -0
- supython/migrations/0002_auth_schema.sql +66 -0
- supython/migrations/0003_demo_todos.sql +42 -0
- supython/migrations/0004_auth_v0_2.sql +47 -0
- supython/migrations/0005_storage_schema.sql +117 -0
- supython/migrations/0006_realtime_schema.sql +206 -0
- supython/migrations/0007_jobs_schema.sql +254 -0
- supython/migrations/0008_jobs_last_error.sql +56 -0
- supython/migrations/0009_auth_rate_limits.sql +33 -0
- supython/migrations/0010_worker_heartbeat.sql +14 -0
- supython/migrations/0011_admin_schema.sql +45 -0
- supython/migrations/0012_auth_banned_until.sql +10 -0
- supython/migrations/0013_email_templates.sql +19 -0
- supython/migrations/0014_realtime_payload_warning.sql +96 -0
- supython/migrations/0015_backups_schema.sql +14 -0
- supython/passwords.py +15 -0
- supython/realtime/__init__.py +6 -0
- supython/realtime/broker.py +814 -0
- supython/realtime/protocol.py +234 -0
- supython/realtime/router.py +184 -0
- supython/realtime/schemas.py +207 -0
- supython/realtime/service.py +261 -0
- supython/realtime/topics.py +175 -0
- supython/realtime/websocket.py +586 -0
- supython/scaffold/__init__.py +5 -0
- supython/scaffold/init_project.py +133 -0
- supython/scaffold/templates/Caddyfile.tmpl +4 -0
- supython/scaffold/templates/README.md.tmpl +22 -0
- supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
- supython/scaffold/templates/docker-compose.yml.tmpl +41 -0
- supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
- supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
- supython/scaffold/templates/env.example.tmpl +149 -0
- supython/scaffold/templates/functions_README.md.tmpl +21 -0
- supython/scaffold/templates/gitignore.tmpl +14 -0
- supython/scaffold/templates/migrations/.gitkeep +0 -0
- supython/secretset.py +347 -0
- supython/security_headers.py +78 -0
- supython/settings.py +198 -0
- supython/storage/__init__.py +5 -0
- supython/storage/backends.py +392 -0
- supython/storage/router.py +341 -0
- supython/storage/schemas.py +50 -0
- supython/storage/service.py +445 -0
- supython/storage/signing.py +119 -0
- supython/tokens.py +85 -0
- supython-0.5.0.dist-info/METADATA +714 -0
- supython-0.5.0.dist-info/RECORD +188 -0
- supython-0.5.0.dist-info/WHEEL +4 -0
- supython-0.5.0.dist-info/entry_points.txt +2 -0
- supython-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Shared Postgres schema introspection queries used by type generators.
|
|
2
|
+
|
|
3
|
+
Both ``types_py.py`` (Python dataclasses) and ``types_ts.py`` (TypeScript
|
|
4
|
+
``Database`` interface) use these functions to fetch metadata from
|
|
5
|
+
``information_schema`` and ``pg_catalog``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncpg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def _fetch_enums(
|
|
12
|
+
conn: asyncpg.Connection, schemas: list[str]
|
|
13
|
+
) -> dict[tuple[str, str], list[str]]:
|
|
14
|
+
rows = await conn.fetch(
|
|
15
|
+
"""
|
|
16
|
+
select n.nspname as schema,
|
|
17
|
+
t.typname as name,
|
|
18
|
+
e.enumlabel as label
|
|
19
|
+
from pg_type t
|
|
20
|
+
join pg_enum e on e.enumtypid = t.oid
|
|
21
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
22
|
+
where n.nspname = any($1::text[])
|
|
23
|
+
order by n.nspname, t.typname, e.enumsortorder
|
|
24
|
+
""",
|
|
25
|
+
list(schemas),
|
|
26
|
+
)
|
|
27
|
+
out: dict[tuple[str, str], list[str]] = {}
|
|
28
|
+
for r in rows:
|
|
29
|
+
out.setdefault((r["schema"], r["name"]), []).append(r["label"])
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _fetch_tables(
|
|
34
|
+
conn: asyncpg.Connection, schemas: list[str]
|
|
35
|
+
) -> list[tuple[str, str]]:
|
|
36
|
+
rows = await conn.fetch(
|
|
37
|
+
"""
|
|
38
|
+
select table_schema, table_name
|
|
39
|
+
from information_schema.tables
|
|
40
|
+
where table_schema = any($1::text[])
|
|
41
|
+
and table_type in ('BASE TABLE', 'VIEW')
|
|
42
|
+
order by table_schema, table_name
|
|
43
|
+
""",
|
|
44
|
+
list(schemas),
|
|
45
|
+
)
|
|
46
|
+
return [(r["table_schema"], r["table_name"]) for r in rows]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _fetch_columns(
|
|
50
|
+
conn: asyncpg.Connection, schemas: list[str]
|
|
51
|
+
) -> dict[tuple[str, str], list[asyncpg.Record]]:
|
|
52
|
+
rows = await conn.fetch(
|
|
53
|
+
"""
|
|
54
|
+
select c.table_schema,
|
|
55
|
+
c.table_name,
|
|
56
|
+
c.column_name,
|
|
57
|
+
c.ordinal_position,
|
|
58
|
+
c.is_nullable,
|
|
59
|
+
c.data_type,
|
|
60
|
+
c.udt_schema,
|
|
61
|
+
c.udt_name,
|
|
62
|
+
c.column_default,
|
|
63
|
+
c.is_generated,
|
|
64
|
+
c.is_identity,
|
|
65
|
+
e.data_type as element_data_type,
|
|
66
|
+
e.udt_schema as element_udt_schema,
|
|
67
|
+
e.udt_name as element_udt_name
|
|
68
|
+
from information_schema.columns c
|
|
69
|
+
join information_schema.tables t
|
|
70
|
+
on t.table_catalog = c.table_catalog
|
|
71
|
+
and t.table_schema = c.table_schema
|
|
72
|
+
and t.table_name = c.table_name
|
|
73
|
+
left join information_schema.element_types e
|
|
74
|
+
on e.object_catalog = c.table_catalog
|
|
75
|
+
and e.object_schema = c.table_schema
|
|
76
|
+
and e.object_name = c.table_name
|
|
77
|
+
and e.object_type = case t.table_type
|
|
78
|
+
when 'VIEW' then 'VIEW'
|
|
79
|
+
else 'TABLE'
|
|
80
|
+
end
|
|
81
|
+
and e.collection_type_identifier = c.dtd_identifier
|
|
82
|
+
where c.table_schema = any($1::text[])
|
|
83
|
+
and t.table_type in ('BASE TABLE', 'VIEW')
|
|
84
|
+
order by c.table_schema, c.table_name, c.ordinal_position
|
|
85
|
+
""",
|
|
86
|
+
list(schemas),
|
|
87
|
+
)
|
|
88
|
+
grouped: dict[tuple[str, str], list[asyncpg.Record]] = {}
|
|
89
|
+
for r in rows:
|
|
90
|
+
grouped.setdefault((r["table_schema"], r["table_name"]), []).append(r)
|
|
91
|
+
return grouped
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def _fetch_relationships(
|
|
95
|
+
conn: asyncpg.Connection, schemas: list[str]
|
|
96
|
+
) -> dict[tuple[str, str], list[dict[str, any]]]: # type: ignore[no-any-return]
|
|
97
|
+
rows = await conn.fetch(
|
|
98
|
+
"""
|
|
99
|
+
select
|
|
100
|
+
n.nspname as table_schema,
|
|
101
|
+
c.relname as table_name,
|
|
102
|
+
con.conname as constraint_name,
|
|
103
|
+
(
|
|
104
|
+
select array_agg(a.attname order by ord)
|
|
105
|
+
from unnest(con.conkey) with ordinality as t(attnum, ord)
|
|
106
|
+
join pg_attribute a
|
|
107
|
+
on a.attnum = t.attnum and a.attrelid = con.conrelid
|
|
108
|
+
) as columns,
|
|
109
|
+
ref_c.relname as foreign_table_name,
|
|
110
|
+
(
|
|
111
|
+
select array_agg(a.attname order by ord)
|
|
112
|
+
from unnest(con.confkey) with ordinality as t(attnum, ord)
|
|
113
|
+
join pg_attribute a
|
|
114
|
+
on a.attnum = t.attnum and a.attrelid = con.confrelid
|
|
115
|
+
) as foreign_columns
|
|
116
|
+
from pg_constraint con
|
|
117
|
+
join pg_class c on c.oid = con.conrelid
|
|
118
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
119
|
+
join pg_class ref_c on ref_c.oid = con.confrelid
|
|
120
|
+
where con.contype = 'f'
|
|
121
|
+
and n.nspname = any($1::text[])
|
|
122
|
+
order by n.nspname, c.relname, con.conname
|
|
123
|
+
""",
|
|
124
|
+
list(schemas),
|
|
125
|
+
)
|
|
126
|
+
out: dict[tuple[str, str], list[dict[str, any]]] = {} # type: ignore[no-any-return]
|
|
127
|
+
for r in rows:
|
|
128
|
+
key = (r["table_schema"], r["table_name"])
|
|
129
|
+
out.setdefault(key, []).append(
|
|
130
|
+
{
|
|
131
|
+
"foreignKeyName": r["constraint_name"],
|
|
132
|
+
"columns": r["columns"],
|
|
133
|
+
"referencedRelation": r["foreign_table_name"],
|
|
134
|
+
"referencedColumns": r["foreign_columns"],
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
return out
|
supython/gen/types_py.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Render a typed ``types.py`` from Postgres schema introspection.
|
|
2
|
+
|
|
3
|
+
Public entry point: :func:`render_types_py`. The returned string is the full
|
|
4
|
+
contents of a self-contained Python module with one ``StrEnum`` per Postgres
|
|
5
|
+
enum and one ``@dataclass`` + matching ``TypedDict`` per table or view in the
|
|
6
|
+
selected schemas.
|
|
7
|
+
|
|
8
|
+
The generator opens its own asyncpg connection and closes it before returning;
|
|
9
|
+
it does not depend on the supython service running.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import keyword
|
|
13
|
+
import re
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
|
|
16
|
+
import asyncpg
|
|
17
|
+
|
|
18
|
+
from ..settings import get_settings
|
|
19
|
+
from ._introspect import _fetch_columns, _fetch_enums, _fetch_tables
|
|
20
|
+
|
|
21
|
+
_SIMPLE_MAP: dict[str, tuple[str, str | None]] = {
|
|
22
|
+
"text": ("str", None),
|
|
23
|
+
"varchar": ("str", None),
|
|
24
|
+
"bpchar": ("str", None),
|
|
25
|
+
"char": ("str", None),
|
|
26
|
+
"citext": ("str", None),
|
|
27
|
+
"name": ("str", None),
|
|
28
|
+
"int2": ("int", None),
|
|
29
|
+
"int4": ("int", None),
|
|
30
|
+
"int8": ("int", None),
|
|
31
|
+
"float4": ("float", None),
|
|
32
|
+
"float8": ("float", None),
|
|
33
|
+
"numeric": ("Decimal", "Decimal"),
|
|
34
|
+
"money": ("Decimal", "Decimal"),
|
|
35
|
+
"bool": ("bool", None),
|
|
36
|
+
"uuid": ("UUID", "UUID"),
|
|
37
|
+
"timestamptz": ("datetime", "datetime"),
|
|
38
|
+
"timestamp": ("datetime", "datetime"),
|
|
39
|
+
"date": ("date", "date"),
|
|
40
|
+
"time": ("time", "time"),
|
|
41
|
+
"timetz": ("time", "time"),
|
|
42
|
+
"interval": ("timedelta", "timedelta"),
|
|
43
|
+
"bytea": ("bytes", None),
|
|
44
|
+
"json": ("dict[str, Any]", "Any"),
|
|
45
|
+
"jsonb": ("dict[str, Any]", "Any"),
|
|
46
|
+
"inet": ("str", None),
|
|
47
|
+
"cidr": ("str", None),
|
|
48
|
+
"macaddr": ("str", None),
|
|
49
|
+
"tsvector": ("str", None),
|
|
50
|
+
"tsquery": ("str", None),
|
|
51
|
+
"bit": ("str", None),
|
|
52
|
+
"varbit": ("str", None),
|
|
53
|
+
"oid": ("int", None),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _safe_col_name(name: str) -> str:
|
|
58
|
+
if keyword.iskeyword(name) or keyword.issoftkeyword(name):
|
|
59
|
+
return f"{name}_"
|
|
60
|
+
return name
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def render_types_py(schemas: list[str]) -> str:
|
|
64
|
+
"""Connect to ``DATABASE_URL`` and return a rendered ``types.py`` module."""
|
|
65
|
+
s = get_settings()
|
|
66
|
+
conn = await asyncpg.connect(s.database_url)
|
|
67
|
+
try:
|
|
68
|
+
enums = await _fetch_enums(conn, schemas)
|
|
69
|
+
tables = await _fetch_tables(conn, schemas)
|
|
70
|
+
columns = await _fetch_columns(conn, schemas)
|
|
71
|
+
finally:
|
|
72
|
+
await conn.close()
|
|
73
|
+
return _render(schemas, enums, tables, columns)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _class_name(schema: str, table: str, schemas: list[str]) -> str:
|
|
78
|
+
base = "".join(part.capitalize() for part in re.split(r"[_\-]", table) if part)
|
|
79
|
+
if not base:
|
|
80
|
+
base = "Table"
|
|
81
|
+
if schema == "public" or len(schemas) == 1:
|
|
82
|
+
return base
|
|
83
|
+
return f"{schema.capitalize()}{base}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _safe_enum_attr(label: str) -> str:
|
|
87
|
+
s = re.sub(r"[^A-Za-z0-9_]", "_", label).upper()
|
|
88
|
+
if not s or s[0].isdigit():
|
|
89
|
+
s = "_" + s
|
|
90
|
+
return s
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _pg_to_py(
|
|
94
|
+
udt_schema: str,
|
|
95
|
+
udt_name: str,
|
|
96
|
+
data_type: str,
|
|
97
|
+
element: tuple[str, str, str] | None,
|
|
98
|
+
enum_classes: dict[tuple[str, str], str],
|
|
99
|
+
imports: set[str],
|
|
100
|
+
) -> tuple[str, str | None]:
|
|
101
|
+
"""Return (annotation, unmapped_comment_or_None)."""
|
|
102
|
+
if data_type == "ARRAY" and element is not None:
|
|
103
|
+
elem_ann, elem_unmapped = _pg_to_py(
|
|
104
|
+
element[1], element[2], element[0], None, enum_classes, imports
|
|
105
|
+
)
|
|
106
|
+
return f"list[{elem_ann}]", elem_unmapped
|
|
107
|
+
|
|
108
|
+
if data_type == "USER-DEFINED" and (udt_schema, udt_name) in enum_classes:
|
|
109
|
+
return enum_classes[(udt_schema, udt_name)], None
|
|
110
|
+
|
|
111
|
+
if udt_name in _SIMPLE_MAP:
|
|
112
|
+
ann, imp = _SIMPLE_MAP[udt_name]
|
|
113
|
+
if imp:
|
|
114
|
+
imports.add(imp)
|
|
115
|
+
return ann, None
|
|
116
|
+
|
|
117
|
+
imports.add("Any")
|
|
118
|
+
return "Any", f"unmapped: {udt_schema}.{udt_name}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _render(
|
|
122
|
+
schemas: list[str],
|
|
123
|
+
enums: dict[tuple[str, str], list[str]],
|
|
124
|
+
tables: list[tuple[str, str]],
|
|
125
|
+
columns: dict[tuple[str, str], list[asyncpg.Record]],
|
|
126
|
+
) -> str:
|
|
127
|
+
imports: set[str] = set()
|
|
128
|
+
|
|
129
|
+
enum_classes: dict[tuple[str, str], str] = {
|
|
130
|
+
(schema, name): _class_name(schema, name, schemas)
|
|
131
|
+
for (schema, name) in enums
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
body: list[str] = []
|
|
135
|
+
|
|
136
|
+
for (schema, name), labels in sorted(enums.items()):
|
|
137
|
+
cls = enum_classes[(schema, name)]
|
|
138
|
+
body.append("")
|
|
139
|
+
body.append(f"# --- enum {schema}.{name} {'-' * (60 - len(schema) - len(name))}")
|
|
140
|
+
body.append("")
|
|
141
|
+
body.append(f"class {cls}(StrEnum):")
|
|
142
|
+
for lbl in labels:
|
|
143
|
+
body.append(f" {_safe_enum_attr(lbl)} = {lbl!r}")
|
|
144
|
+
|
|
145
|
+
has_table = False
|
|
146
|
+
for schema, table in tables:
|
|
147
|
+
cols = columns.get((schema, table), [])
|
|
148
|
+
if not cols:
|
|
149
|
+
continue
|
|
150
|
+
has_table = True
|
|
151
|
+
cls = _class_name(schema, table, schemas)
|
|
152
|
+
|
|
153
|
+
rendered_cols: list[tuple[str, str, bool, str | None]] = []
|
|
154
|
+
for c in cols:
|
|
155
|
+
element = None
|
|
156
|
+
if c["element_data_type"]:
|
|
157
|
+
element = (
|
|
158
|
+
c["element_data_type"],
|
|
159
|
+
c["element_udt_schema"],
|
|
160
|
+
c["element_udt_name"],
|
|
161
|
+
)
|
|
162
|
+
ann, unmapped = _pg_to_py(
|
|
163
|
+
c["udt_schema"],
|
|
164
|
+
c["udt_name"],
|
|
165
|
+
c["data_type"],
|
|
166
|
+
element,
|
|
167
|
+
enum_classes,
|
|
168
|
+
imports,
|
|
169
|
+
)
|
|
170
|
+
nullable = c["is_nullable"] == "YES"
|
|
171
|
+
col_name = _safe_col_name(c["column_name"])
|
|
172
|
+
rendered_cols.append((col_name, ann, nullable, unmapped))
|
|
173
|
+
|
|
174
|
+
body.append("")
|
|
175
|
+
body.append(f"# --- {schema}.{table} {'-' * (64 - len(schema) - len(table))}")
|
|
176
|
+
body.append("")
|
|
177
|
+
body.append("@dataclass(kw_only=True, slots=True)")
|
|
178
|
+
body.append(f"class {cls}:")
|
|
179
|
+
for col_name, ann, nullable, unmapped in rendered_cols:
|
|
180
|
+
line = (
|
|
181
|
+
f" {col_name}: {ann} | None = None"
|
|
182
|
+
if nullable
|
|
183
|
+
else f" {col_name}: {ann}"
|
|
184
|
+
)
|
|
185
|
+
if unmapped:
|
|
186
|
+
line += f" # {unmapped}"
|
|
187
|
+
body.append(line)
|
|
188
|
+
|
|
189
|
+
body.append("")
|
|
190
|
+
body.append(" @classmethod")
|
|
191
|
+
body.append(f' def from_record(cls, record: "object") -> "{cls}":')
|
|
192
|
+
body.append(" fields = cls.__dataclass_fields__")
|
|
193
|
+
body.append(
|
|
194
|
+
" return cls(**{f: v for f, v in record.items() if f in fields})"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
has_kw = any(
|
|
198
|
+
_safe_col_name(c["column_name"]) != c["column_name"]
|
|
199
|
+
for c in cols
|
|
200
|
+
)
|
|
201
|
+
if has_kw:
|
|
202
|
+
_emit_typeddict_functional(body, cls, rendered_cols)
|
|
203
|
+
else:
|
|
204
|
+
_emit_typeddict_class(body, cls, rendered_cols)
|
|
205
|
+
|
|
206
|
+
header: list[str] = []
|
|
207
|
+
header.append('"""Generated by `supython gen types --lang py`. Do not edit.')
|
|
208
|
+
header.append("")
|
|
209
|
+
header.append(f"Schemas: {', '.join(schemas)}")
|
|
210
|
+
header.append(f"Generated at: {datetime.now(UTC).isoformat()}")
|
|
211
|
+
header.append('"""')
|
|
212
|
+
|
|
213
|
+
import_lines: list[str] = []
|
|
214
|
+
if has_table:
|
|
215
|
+
import_lines.append("from dataclasses import dataclass")
|
|
216
|
+
|
|
217
|
+
datetime_syms = sorted(
|
|
218
|
+
{i for i in imports if i in {"datetime", "date", "time", "timedelta"}}
|
|
219
|
+
)
|
|
220
|
+
if datetime_syms:
|
|
221
|
+
import_lines.append(f"from datetime import {', '.join(datetime_syms)}")
|
|
222
|
+
if "Decimal" in imports:
|
|
223
|
+
import_lines.append("from decimal import Decimal")
|
|
224
|
+
if enums:
|
|
225
|
+
import_lines.append("from enum import StrEnum")
|
|
226
|
+
typing_syms: set[str] = set()
|
|
227
|
+
if "Any" in imports:
|
|
228
|
+
typing_syms.add("Any")
|
|
229
|
+
if has_table:
|
|
230
|
+
typing_syms.add("TypedDict")
|
|
231
|
+
if typing_syms:
|
|
232
|
+
import_lines.append(f"from typing import {', '.join(sorted(typing_syms))}")
|
|
233
|
+
if "UUID" in imports:
|
|
234
|
+
import_lines.append("from uuid import UUID")
|
|
235
|
+
|
|
236
|
+
parts: list[str] = []
|
|
237
|
+
parts.extend(header)
|
|
238
|
+
if import_lines:
|
|
239
|
+
parts.append("")
|
|
240
|
+
parts.extend(import_lines)
|
|
241
|
+
parts.extend(body)
|
|
242
|
+
return "\n".join(parts).rstrip() + "\n"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _emit_typeddict_class(
|
|
246
|
+
body: list[str],
|
|
247
|
+
cls: str,
|
|
248
|
+
rendered_cols: list[tuple[str, str, bool, str | None]],
|
|
249
|
+
) -> None:
|
|
250
|
+
body.append("")
|
|
251
|
+
body.append(f"class {cls}Row(TypedDict):")
|
|
252
|
+
for col_name, ann, nullable, _ in rendered_cols:
|
|
253
|
+
body.append(
|
|
254
|
+
f" {col_name}: {ann} | None" if nullable else f" {col_name}: {ann}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _emit_typeddict_functional(
|
|
259
|
+
body: list[str],
|
|
260
|
+
cls: str,
|
|
261
|
+
rendered_cols: list[tuple[str, str, bool, str | None]],
|
|
262
|
+
) -> None:
|
|
263
|
+
body.append(f"{cls}Row = TypedDict(\"{cls}Row\", {{")
|
|
264
|
+
for col_name, ann, nullable, _ in rendered_cols:
|
|
265
|
+
ann_str = f"{ann} | None" if nullable else ann
|
|
266
|
+
body.append(f' "{col_name}": {ann_str},')
|
|
267
|
+
body.append("})")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
__all__ = ["render_types_py"]
|