paskia 0.8.1__py3-none-any.whl → 0.9.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.
- paskia/_version.py +2 -2
- paskia/authsession.py +14 -27
- paskia/bootstrap.py +31 -103
- paskia/db/__init__.py +25 -51
- paskia/db/background.py +17 -37
- paskia/db/jsonl.py +168 -6
- paskia/db/migrations.py +34 -0
- paskia/db/operations.py +400 -723
- paskia/db/structs.py +214 -90
- paskia/fastapi/__main__.py +24 -28
- paskia/fastapi/admin.py +101 -160
- paskia/fastapi/api.py +47 -83
- paskia/fastapi/mainapp.py +13 -6
- paskia/fastapi/remote.py +16 -39
- paskia/fastapi/reset.py +27 -17
- paskia/fastapi/session.py +2 -2
- paskia/fastapi/user.py +21 -27
- paskia/fastapi/ws.py +27 -62
- paskia/fastapi/wschat.py +62 -0
- paskia/frontend-build/auth/admin/index.html +5 -5
- paskia/frontend-build/auth/assets/{AccessDenied-Bc249ASC.css → AccessDenied-DPkUS8LZ.css} +1 -1
- paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js +8 -0
- paskia/frontend-build/auth/assets/{RestrictedAuth-DgdJyscT.css → RestrictedAuth-CvR33_Z0.css} +1 -1
- paskia/frontend-build/auth/assets/RestrictedAuth-DsJXicIw.js +1 -0
- paskia/frontend-build/auth/assets/{_plugin-vue_export-helper-rKFEraYH.js → _plugin-vue_export-helper-nhjnO_bd.js} +1 -1
- paskia/frontend-build/auth/assets/admin-CPE1pLMm.js +1 -0
- paskia/frontend-build/auth/assets/{admin-BeNu48FR.css → admin-DzzjSg72.css} +1 -1
- paskia/frontend-build/auth/assets/{auth-BKX7shEe.css → auth-C7k64Wad.css} +1 -1
- paskia/frontend-build/auth/assets/auth-YIZvPlW_.js +1 -0
- paskia/frontend-build/auth/assets/{forward-Dzg-aE1C.js → forward-DmqVHZ7e.js} +1 -1
- paskia/frontend-build/auth/assets/reset-Chtv69AT.css +1 -0
- paskia/frontend-build/auth/assets/reset-s20PATTN.js +1 -0
- paskia/frontend-build/auth/assets/{restricted-C0IQufuH.js → restricted-D3AJx3_6.js} +1 -1
- paskia/frontend-build/auth/index.html +5 -5
- paskia/frontend-build/auth/restricted/index.html +4 -4
- paskia/frontend-build/int/forward/index.html +4 -4
- paskia/frontend-build/int/reset/index.html +3 -3
- paskia/globals.py +2 -2
- paskia/migrate/__init__.py +62 -55
- paskia/migrate/sql.py +72 -22
- paskia/remoteauth.py +1 -2
- paskia/sansio.py +6 -12
- {paskia-0.8.1.dist-info → paskia-0.9.0.dist-info}/METADATA +1 -1
- paskia-0.9.0.dist-info/RECORD +57 -0
- paskia/frontend-build/auth/assets/AccessDenied-aTdCvz9k.js +0 -8
- paskia/frontend-build/auth/assets/RestrictedAuth-BLMK7-nL.js +0 -1
- paskia/frontend-build/auth/assets/admin-tVs8oyLv.js +0 -1
- paskia/frontend-build/auth/assets/auth-Dk3q4pNS.js +0 -1
- paskia/frontend-build/auth/assets/reset-BWF4cWKR.css +0 -1
- paskia/frontend-build/auth/assets/reset-C_Td1_jn.js +0 -1
- paskia-0.8.1.dist-info/RECORD +0 -55
- {paskia-0.8.1.dist-info → paskia-0.9.0.dist-info}/WHEEL +0 -0
- {paskia-0.8.1.dist-info → paskia-0.9.0.dist-info}/entry_points.txt +0 -0
paskia/db/structs.py
CHANGED
|
@@ -1,43 +1,149 @@
|
|
|
1
|
-
from datetime import datetime
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
2
|
from uuid import UUID
|
|
3
3
|
|
|
4
4
|
import msgspec
|
|
5
|
+
import uuid7
|
|
5
6
|
|
|
7
|
+
# Sentinel for uuid fields before they are set by create() or DB post init
|
|
8
|
+
_UUID_UNSET = UUID(int=0)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Permission(msgspec.Struct, dict=True, omit_defaults=True):
|
|
12
|
+
"""Permission data structure.
|
|
13
|
+
|
|
14
|
+
Mutable fields: scope, display_name, domain, orgs
|
|
15
|
+
Immutable fields: None (all fields can be updated via update_permission)
|
|
16
|
+
uuid is generated at creation.
|
|
17
|
+
"""
|
|
6
18
|
|
|
7
|
-
class Permission(msgspec.Struct, omit_defaults=True):
|
|
8
|
-
uuid: UUID # UUID primary key
|
|
9
19
|
scope: str # Permission scope identifier (e.g. "auth:admin", "myapp:write")
|
|
10
20
|
display_name: str
|
|
11
21
|
domain: str | None = None # If set, scopes permission to this domain
|
|
22
|
+
orgs: dict[UUID, bool] = {} # org_uuid -> True (which orgs can grant this)
|
|
12
23
|
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
self.uuid: UUID = _UUID_UNSET # Convenience field, not serialized
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def org_set(self) -> set[UUID]:
|
|
29
|
+
"""Get orgs that can grant this permission as a set."""
|
|
30
|
+
return set(self.orgs.keys())
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def create(
|
|
34
|
+
cls,
|
|
35
|
+
scope: str,
|
|
36
|
+
display_name: str,
|
|
37
|
+
domain: str | None = None,
|
|
38
|
+
) -> "Permission":
|
|
39
|
+
"""Create a new Permission with auto-generated uuid7."""
|
|
40
|
+
perm = cls(
|
|
41
|
+
scope=scope,
|
|
42
|
+
display_name=display_name,
|
|
43
|
+
domain=domain,
|
|
44
|
+
)
|
|
45
|
+
perm.uuid = uuid7.create()
|
|
46
|
+
return perm
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Role(msgspec.Struct, dict=True, omit_defaults=True):
|
|
50
|
+
"""Role data structure.
|
|
51
|
+
|
|
52
|
+
Mutable fields: display_name, permissions
|
|
53
|
+
Immutable fields: org (set at creation, never modified)
|
|
54
|
+
uuid is generated at creation.
|
|
55
|
+
"""
|
|
13
56
|
|
|
14
|
-
|
|
15
|
-
uuid: UUID
|
|
16
|
-
org_uuid: UUID
|
|
57
|
+
org: UUID
|
|
17
58
|
display_name: str
|
|
18
|
-
permissions:
|
|
59
|
+
permissions: dict[UUID, bool] = {} # permission_uuid -> True
|
|
19
60
|
|
|
61
|
+
def __post_init__(self):
|
|
62
|
+
self.uuid: UUID = _UUID_UNSET # Convenience field, not serialized
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def permission_set(self) -> set[UUID]:
|
|
66
|
+
"""Get permissions as a set of UUIDs."""
|
|
67
|
+
return set(self.permissions.keys())
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def create(
|
|
71
|
+
cls,
|
|
72
|
+
org: UUID,
|
|
73
|
+
display_name: str,
|
|
74
|
+
permissions: set[UUID] | None = None,
|
|
75
|
+
) -> "Role":
|
|
76
|
+
"""Create a new Role with auto-generated uuid7."""
|
|
77
|
+
role = cls(
|
|
78
|
+
org=org,
|
|
79
|
+
display_name=display_name,
|
|
80
|
+
permissions={p: True for p in (permissions or set())},
|
|
81
|
+
)
|
|
82
|
+
role.uuid = uuid7.create()
|
|
83
|
+
return role
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Org(msgspec.Struct, dict=True):
|
|
87
|
+
"""Organization data structure."""
|
|
20
88
|
|
|
21
|
-
class Org(msgspec.Struct):
|
|
22
|
-
uuid: UUID
|
|
23
89
|
display_name: str
|
|
24
|
-
|
|
25
|
-
|
|
90
|
+
|
|
91
|
+
def __post_init__(self):
|
|
92
|
+
self.uuid: UUID = _UUID_UNSET # Convenience field, not serialized
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def create(cls, display_name: str) -> "Org":
|
|
96
|
+
"""Create a new Org with auto-generated uuid7."""
|
|
97
|
+
org = cls(display_name=display_name)
|
|
98
|
+
org.uuid = uuid7.create()
|
|
99
|
+
return org
|
|
26
100
|
|
|
27
101
|
|
|
28
|
-
class User(msgspec.Struct):
|
|
29
|
-
|
|
102
|
+
class User(msgspec.Struct, dict=True):
|
|
103
|
+
"""User data structure.
|
|
104
|
+
|
|
105
|
+
Mutable fields: display_name, role, last_seen, visits
|
|
106
|
+
Immutable fields: created_at (set at creation, never modified)
|
|
107
|
+
uuid is derived from created_at using uuid7.
|
|
108
|
+
"""
|
|
109
|
+
|
|
30
110
|
display_name: str
|
|
31
|
-
|
|
32
|
-
created_at: datetime
|
|
111
|
+
role: UUID
|
|
112
|
+
created_at: datetime
|
|
33
113
|
last_seen: datetime | None = None
|
|
34
114
|
visits: int = 0
|
|
35
115
|
|
|
116
|
+
def __post_init__(self):
|
|
117
|
+
self.uuid: UUID = _UUID_UNSET # Convenience field, not serialized
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def create(
|
|
121
|
+
cls,
|
|
122
|
+
display_name: str,
|
|
123
|
+
role: UUID,
|
|
124
|
+
created_at: datetime | None = None,
|
|
125
|
+
) -> "User":
|
|
126
|
+
"""Create a new User with auto-generated uuid7."""
|
|
127
|
+
|
|
128
|
+
user = cls(
|
|
129
|
+
display_name=display_name,
|
|
130
|
+
role=role,
|
|
131
|
+
created_at=created_at or datetime.now(timezone.utc),
|
|
132
|
+
)
|
|
133
|
+
user.uuid = uuid7.create(user.created_at)
|
|
134
|
+
return user
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Credential(msgspec.Struct, dict=True):
|
|
138
|
+
"""Credential (passkey) data structure.
|
|
139
|
+
|
|
140
|
+
Mutable fields: sign_count, last_used, last_verified
|
|
141
|
+
Immutable fields: credential_id, user, aaguid, public_key, created_at
|
|
142
|
+
uuid is derived from created_at using uuid7.
|
|
143
|
+
"""
|
|
36
144
|
|
|
37
|
-
class Credential(msgspec.Struct):
|
|
38
|
-
uuid: UUID
|
|
39
145
|
credential_id: bytes # Long binary ID from the authenticator
|
|
40
|
-
|
|
146
|
+
user: UUID
|
|
41
147
|
aaguid: UUID
|
|
42
148
|
public_key: bytes
|
|
43
149
|
sign_count: int
|
|
@@ -45,16 +151,53 @@ class Credential(msgspec.Struct):
|
|
|
45
151
|
last_used: datetime | None = None
|
|
46
152
|
last_verified: datetime | None = None
|
|
47
153
|
|
|
154
|
+
def __post_init__(self):
|
|
155
|
+
self.uuid: UUID = _UUID_UNSET # Convenience field, not serialized
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def create(
|
|
159
|
+
cls,
|
|
160
|
+
credential_id: bytes,
|
|
161
|
+
user: UUID,
|
|
162
|
+
aaguid: UUID,
|
|
163
|
+
public_key: bytes,
|
|
164
|
+
sign_count: int,
|
|
165
|
+
created_at: datetime | None = None,
|
|
166
|
+
) -> "Credential":
|
|
167
|
+
"""Create a new Credential with auto-generated uuid7."""
|
|
168
|
+
now = created_at or datetime.now(timezone.utc)
|
|
169
|
+
cred = cls(
|
|
170
|
+
credential_id=credential_id,
|
|
171
|
+
user=user,
|
|
172
|
+
aaguid=aaguid,
|
|
173
|
+
public_key=public_key,
|
|
174
|
+
sign_count=sign_count,
|
|
175
|
+
created_at=now,
|
|
176
|
+
last_used=now,
|
|
177
|
+
last_verified=now,
|
|
178
|
+
)
|
|
179
|
+
cred.uuid = uuid7.create(now)
|
|
180
|
+
return cred
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class Session(msgspec.Struct, dict=True):
|
|
184
|
+
"""Session data structure.
|
|
185
|
+
|
|
186
|
+
Mutable fields: expiry (updated on session refresh)
|
|
187
|
+
Immutable fields: user, credential, host, ip, user_agent
|
|
188
|
+
key is stored in the dict key, not in the struct.
|
|
189
|
+
"""
|
|
48
190
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
ip: str | None
|
|
55
|
-
user_agent: str | None
|
|
191
|
+
user: UUID
|
|
192
|
+
credential: UUID
|
|
193
|
+
host: str
|
|
194
|
+
ip: str
|
|
195
|
+
user_agent: str
|
|
56
196
|
expiry: datetime
|
|
57
197
|
|
|
198
|
+
def __post_init__(self):
|
|
199
|
+
self.key: str = "" # Convenience field, not serialized
|
|
200
|
+
|
|
58
201
|
def metadata(self) -> dict:
|
|
59
202
|
"""Return session metadata for backwards compatibility."""
|
|
60
203
|
return {
|
|
@@ -64,85 +207,66 @@ class Session(msgspec.Struct):
|
|
|
64
207
|
}
|
|
65
208
|
|
|
66
209
|
|
|
67
|
-
class ResetToken(msgspec.Struct):
|
|
68
|
-
|
|
69
|
-
|
|
210
|
+
class ResetToken(msgspec.Struct, dict=True):
|
|
211
|
+
"""Reset/device-addition token data structure.
|
|
212
|
+
|
|
213
|
+
Immutable fields: All fields (tokens are created and deleted, never modified)
|
|
214
|
+
key is stored in the dict key, not in the struct.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
user: UUID
|
|
70
218
|
expiry: datetime
|
|
71
219
|
token_type: str
|
|
72
220
|
|
|
221
|
+
def __post_init__(self):
|
|
222
|
+
self.key: bytes = b"" # Convenience field, not serialized
|
|
223
|
+
|
|
73
224
|
|
|
74
225
|
class SessionContext(msgspec.Struct):
|
|
75
226
|
session: Session
|
|
76
227
|
user: User
|
|
77
228
|
org: Org
|
|
78
229
|
role: Role
|
|
79
|
-
credential: Credential
|
|
80
|
-
permissions: list[Permission]
|
|
230
|
+
credential: Credential
|
|
231
|
+
permissions: list[Permission] = []
|
|
81
232
|
|
|
82
233
|
|
|
83
234
|
# -------------------------------------------------------------------------
|
|
84
|
-
#
|
|
235
|
+
# Database storage structure
|
|
85
236
|
# -------------------------------------------------------------------------
|
|
86
237
|
|
|
87
238
|
|
|
88
|
-
class
|
|
89
|
-
|
|
90
|
-
display_name: str
|
|
91
|
-
domain: str | None = None
|
|
92
|
-
orgs: dict[UUID, bool] = {} # org_uuid -> True (which orgs can grant this)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class _OrgData(msgspec.Struct):
|
|
96
|
-
display_name: str
|
|
97
|
-
created_at: datetime | None = None
|
|
239
|
+
class DB(msgspec.Struct, dict=True, omit_defaults=False):
|
|
240
|
+
"""In-memory database. Access fields directly for reads."""
|
|
98
241
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class _UserData(msgspec.Struct):
|
|
107
|
-
display_name: str
|
|
108
|
-
role: UUID
|
|
109
|
-
created_at: datetime
|
|
110
|
-
last_seen: datetime | None
|
|
111
|
-
visits: int
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class _CredentialData(msgspec.Struct):
|
|
115
|
-
credential_id: bytes
|
|
116
|
-
user: UUID
|
|
117
|
-
aaguid: UUID
|
|
118
|
-
public_key: bytes
|
|
119
|
-
sign_count: int
|
|
120
|
-
created_at: datetime
|
|
121
|
-
last_used: datetime | None
|
|
122
|
-
last_verified: datetime | None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class _SessionData(msgspec.Struct):
|
|
126
|
-
user: UUID
|
|
127
|
-
credential: UUID
|
|
128
|
-
host: str | None
|
|
129
|
-
ip: str | None
|
|
130
|
-
user_agent: str | None
|
|
131
|
-
expiry: datetime
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class _ResetTokenData(msgspec.Struct):
|
|
135
|
-
user: UUID
|
|
136
|
-
expiry: datetime
|
|
137
|
-
token_type: str
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class _DatabaseData(msgspec.Struct, omit_defaults=True):
|
|
141
|
-
permissions: dict[UUID, _PermissionData]
|
|
142
|
-
orgs: dict[UUID, _OrgData]
|
|
143
|
-
roles: dict[UUID, _RoleData]
|
|
144
|
-
users: dict[UUID, _UserData]
|
|
145
|
-
credentials: dict[UUID, _CredentialData]
|
|
146
|
-
sessions: dict[str, _SessionData]
|
|
147
|
-
reset_tokens: dict[bytes, _ResetTokenData]
|
|
242
|
+
permissions: dict[UUID, Permission] = {}
|
|
243
|
+
orgs: dict[UUID, Org] = {}
|
|
244
|
+
roles: dict[UUID, Role] = {}
|
|
245
|
+
users: dict[UUID, User] = {}
|
|
246
|
+
credentials: dict[UUID, Credential] = {}
|
|
247
|
+
sessions: dict[str, Session] = {}
|
|
248
|
+
reset_tokens: dict[bytes, ResetToken] = {}
|
|
148
249
|
v: int = 0
|
|
250
|
+
|
|
251
|
+
def __post_init__(self):
|
|
252
|
+
# Store reference for persistence (not serialized)
|
|
253
|
+
self._store = None
|
|
254
|
+
# Set the key fields on all stored objects
|
|
255
|
+
for uuid, perm in self.permissions.items():
|
|
256
|
+
perm.uuid = uuid
|
|
257
|
+
for uuid, org in self.orgs.items():
|
|
258
|
+
org.uuid = uuid
|
|
259
|
+
for uuid, role in self.roles.items():
|
|
260
|
+
role.uuid = uuid
|
|
261
|
+
for uuid, user in self.users.items():
|
|
262
|
+
user.uuid = uuid
|
|
263
|
+
for uuid, cred in self.credentials.items():
|
|
264
|
+
cred.uuid = uuid
|
|
265
|
+
for key, session in self.sessions.items():
|
|
266
|
+
session.key = key
|
|
267
|
+
for key, token in self.reset_tokens.items():
|
|
268
|
+
token.key = key
|
|
269
|
+
|
|
270
|
+
def transaction(self, action, ctx=None, *, user=None):
|
|
271
|
+
"""Wrap writes in transaction. Delegates to JsonlStore."""
|
|
272
|
+
return self._store.transaction(action, ctx, user=user)
|
paskia/fastapi/__main__.py
CHANGED
|
@@ -5,13 +5,13 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
|
|
8
|
-
import uvicorn
|
|
9
8
|
from fastapi_vue.hostutil import parse_endpoint
|
|
10
9
|
from uvicorn import Config, Server
|
|
11
10
|
|
|
12
11
|
from paskia import globals as _globals
|
|
13
12
|
from paskia.bootstrap import bootstrap_if_needed
|
|
14
13
|
from paskia.config import PaskiaConfig
|
|
14
|
+
from paskia.db.background import flush
|
|
15
15
|
from paskia.fastapi import app as fastapi_app
|
|
16
16
|
from paskia.fastapi import reset as reset_cmd
|
|
17
17
|
from paskia.util import startupbox
|
|
@@ -183,28 +183,8 @@ def main():
|
|
|
183
183
|
}
|
|
184
184
|
os.environ["PASKIA_CONFIG"] = json.dumps(config_json)
|
|
185
185
|
|
|
186
|
-
# Initialize globals (without bootstrap yet)
|
|
187
|
-
asyncio.run(
|
|
188
|
-
_globals.init(
|
|
189
|
-
rp_id=config.rp_id,
|
|
190
|
-
rp_name=config.rp_name,
|
|
191
|
-
origins=config.origins,
|
|
192
|
-
bootstrap=False,
|
|
193
|
-
)
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
# Print startup configuration
|
|
197
186
|
startupbox.print_startup_config(config)
|
|
198
187
|
|
|
199
|
-
# Bootstrap after startup box is printed
|
|
200
|
-
asyncio.run(bootstrap_if_needed())
|
|
201
|
-
|
|
202
|
-
# Handle reset command (no server start)
|
|
203
|
-
if is_reset:
|
|
204
|
-
exit_code = reset_cmd.run(args.reset_query)
|
|
205
|
-
raise SystemExit(exit_code)
|
|
206
|
-
|
|
207
|
-
# Dev mode: enable reload when FASTAPI_VUE_FRONTEND_URL is set
|
|
208
188
|
devmode = bool(os.environ.get("FASTAPI_VUE_FRONTEND_URL"))
|
|
209
189
|
|
|
210
190
|
run_kwargs: dict = {
|
|
@@ -221,18 +201,34 @@ def main():
|
|
|
221
201
|
# Suppress uvicorn startup messages in dev mode
|
|
222
202
|
run_kwargs["log_level"] = "warning"
|
|
223
203
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
204
|
+
async def async_main():
|
|
205
|
+
await _globals.init(
|
|
206
|
+
rp_id=config.rp_id,
|
|
207
|
+
rp_name=config.rp_name,
|
|
208
|
+
origins=config.origins,
|
|
209
|
+
bootstrap=False,
|
|
210
|
+
)
|
|
211
|
+
await bootstrap_if_needed()
|
|
212
|
+
await flush()
|
|
213
|
+
|
|
214
|
+
if is_reset:
|
|
215
|
+
exit_code = reset_cmd.run(args.reset_query)
|
|
216
|
+
raise SystemExit(exit_code)
|
|
217
|
+
|
|
218
|
+
if len(endpoints) > 1:
|
|
227
219
|
async with asyncio.TaskGroup() as tg:
|
|
228
220
|
for ep in endpoints:
|
|
229
221
|
tg.create_task(
|
|
230
222
|
Server(Config(app=fastapi_app, **run_kwargs, **ep)).serve()
|
|
231
223
|
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
224
|
+
else:
|
|
225
|
+
server = Server(Config(app=fastapi_app, **run_kwargs, **endpoints[0]))
|
|
226
|
+
await server.serve()
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
asyncio.run(async_main())
|
|
230
|
+
except KeyboardInterrupt:
|
|
231
|
+
pass
|
|
236
232
|
|
|
237
233
|
|
|
238
234
|
if __name__ == "__main__":
|