vs-server 0.1.2__tar.gz → 0.1.4__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.
- {vs_server-0.1.2/vs_server.egg-info → vs_server-0.1.4}/PKG-INFO +110 -10
- {vs_server-0.1.2 → vs_server-0.1.4}/README.md +109 -9
- {vs_server-0.1.2 → vs_server-0.1.4}/pyproject.toml +1 -1
- vs_server-0.1.4/vs_server/decorator/vs_controller_decorator.py +48 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/server/vs_fast_api_server.py +47 -8
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/server/vs_server.py +0 -9
- {vs_server-0.1.2 → vs_server-0.1.4/vs_server.egg-info}/PKG-INFO +110 -10
- vs_server-0.1.2/vs_server/decorator/vs_controller_decorator.py +0 -44
- {vs_server-0.1.2 → vs_server-0.1.4}/LICENSE.txt +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/setup.cfg +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/tests/test_product_controller.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/tests/test_server.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/tests/test_user_controller.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/decorator/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/decorator/vs_server_decorator.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/decorator/vs_server_registry.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/decorator/vs_sse_decorator.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/decorator/vs_websocket_decorator.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/docs/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/docs/vs_docs_renderer.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/factory/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/factory/vs_server_factory.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/lifecycle/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/lifecycle/vs_lifecycle.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/schema/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/schema/exceptions.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/schema/vs_server_health.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server/server/__init__.py +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server.egg-info/SOURCES.txt +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server.egg-info/dependency_links.txt +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server.egg-info/requires.txt +0 -0
- {vs_server-0.1.2 → vs_server-0.1.4}/vs_server.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vs-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: HTTP server abstraction for Viveka Sutra — FastAPI wrapper with lifecycle, routing, and middleware
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -348,6 +348,52 @@ All server config lives under the `[server]` section of `config.ini`.
|
|
|
348
348
|
| `server.ssl_keyfile_password` | — | Password for encrypted private key |
|
|
349
349
|
| `api.cors_origins` | `*` | Comma-separated allowed CORS origins |
|
|
350
350
|
|
|
351
|
+
These keys apply to all `VsServer` implementations. Framework-specific config lives in its own section.
|
|
352
|
+
|
|
353
|
+
### FastAPI-specific config (`[fastapi]`)
|
|
354
|
+
|
|
355
|
+
| Key | Default | Description |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `fastapi.workers` | `1` | Number of worker processes. Requires `fastapi.app_import` to be set. |
|
|
358
|
+
| `fastapi.reload` | `false` | Enable hot reload (dev only). Requires `fastapi.app_import` to be set. |
|
|
359
|
+
| `fastapi.log_level` | `info` | Uvicorn log level (`debug`, `info`, `warning`, `error`) |
|
|
360
|
+
| `fastapi.app_import` | — | Import string for multi-worker or reload mode, e.g. `my_service.app:server`. Required when `workers > 1` or `reload = true`. |
|
|
361
|
+
|
|
362
|
+
**`config.ini` example for multi-worker production:**
|
|
363
|
+
|
|
364
|
+
```ini
|
|
365
|
+
[server]
|
|
366
|
+
name = my-service
|
|
367
|
+
version = 1.0.0
|
|
368
|
+
host = 0.0.0.0
|
|
369
|
+
port = 8000
|
|
370
|
+
docs_enabled = false
|
|
371
|
+
|
|
372
|
+
[fastapi]
|
|
373
|
+
workers = 4
|
|
374
|
+
log_level = warning
|
|
375
|
+
app_import = my_service.app:server
|
|
376
|
+
|
|
377
|
+
[api]
|
|
378
|
+
cors_origins = https://app.example.com
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**What is `app_import`?**
|
|
382
|
+
|
|
383
|
+
When running multiple workers, uvicorn forks child processes that each need to reconstruct the app independently. They can't share the in-memory object from the parent process — they need an import path to build their own copy.
|
|
384
|
+
|
|
385
|
+
`app_import` is `module.path:variable_name` where `variable_name` is whatever you assigned `VsServerFactory.get()` to:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
# my_service/app.py
|
|
389
|
+
server = VsServerFactory.get("fastapi", config) # → app_import = my_service.app:server
|
|
390
|
+
server.run()
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
If `app_import` is not set, vs-server will attempt to auto-detect it and log it at startup. Copy the logged value into your config to make it explicit.
|
|
394
|
+
|
|
395
|
+
**Custom server implementations** follow the same pattern — add a `[my_framework]` section in `config.ini` for any framework-specific keys and read them in your `VsServer` subclass.
|
|
396
|
+
|
|
351
397
|
**`config.ini` example:**
|
|
352
398
|
|
|
353
399
|
```ini
|
|
@@ -469,6 +515,57 @@ async def get_report():
|
|
|
469
515
|
|
|
470
516
|
---
|
|
471
517
|
|
|
518
|
+
## Request and Response Models
|
|
519
|
+
|
|
520
|
+
All request bodies and response models must be Pydantic models — not SQLAlchemy entities or plain dicts. This is a hard requirement of FastAPI and applies to all server implementations built on vs-server.
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
from pydantic import BaseModel
|
|
524
|
+
|
|
525
|
+
class UserRequest(BaseModel):
|
|
526
|
+
name: str
|
|
527
|
+
email: str
|
|
528
|
+
|
|
529
|
+
class UserResponse(BaseModel):
|
|
530
|
+
id: str
|
|
531
|
+
name: str
|
|
532
|
+
email: str
|
|
533
|
+
|
|
534
|
+
model_config = {"from_attributes": True}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The mapping between your SQLAlchemy entity and your Pydantic schema is your responsibility:
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
Request (Pydantic) → Service → Entity (SQLAlchemy) → DB
|
|
541
|
+
DB → Entity (SQLAlchemy) → Service → Response (Pydantic)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Never pass a SQLAlchemy entity as `response_model` — FastAPI will reject it at startup. Never return a SQLAlchemy entity directly from a route — serialize it first.
|
|
545
|
+
|
|
546
|
+
Keeping schemas separate from entities is intentional: the DB schema and API schema can evolve independently, and sensitive fields (e.g. `password`) are excluded from responses by default.
|
|
547
|
+
|
|
548
|
+
### @model — Reduce Schema Boilerplate (requires vs-db)
|
|
549
|
+
|
|
550
|
+
If you are using `vs-db`, the `@model` decorator derives Pydantic fields directly from a SQLAlchemy entity so you don't repeat type definitions. Nullable columns become `Optional[T] = None` automatically. Fields already defined on the class are never overridden.
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
from pydantic import BaseModel, Field
|
|
554
|
+
from vs_db.decorator.vs_model_decorator import model
|
|
555
|
+
|
|
556
|
+
@model(AppUser, exclude=["password"])
|
|
557
|
+
class UserResponse(BaseModel):
|
|
558
|
+
pass # all AppUser fields injected except password
|
|
559
|
+
|
|
560
|
+
@model(AppUser, include=["email", "first_name", "last_name"])
|
|
561
|
+
class CreateUserRequest(BaseModel):
|
|
562
|
+
first_name: str = Field(min_length=1) # user-defined — decorator skips it
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
See the `vs-db` README for full documentation on `@model`.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
472
569
|
## HTTP Controllers
|
|
473
570
|
|
|
474
571
|
Annotate a class with `@controller` and methods with HTTP verb decorators.
|
|
@@ -520,11 +617,11 @@ All FastAPI route parameters (`response_model`, `status_code`, `tags`, `summary`
|
|
|
520
617
|
|
|
521
618
|
| Decorator | HTTP Method |
|
|
522
619
|
|---|---|
|
|
523
|
-
| `@get(path, guards=[], **kwargs)` | GET |
|
|
524
|
-
| `@post(path, guards=[], **kwargs)` | POST |
|
|
525
|
-
| `@put(path, guards=[], **kwargs)` | PUT |
|
|
526
|
-
| `@delete(path, guards=[], **kwargs)` | DELETE |
|
|
527
|
-
| `@patch(path, guards=[], **kwargs)` | PATCH |
|
|
620
|
+
| `@get(path, summary, description, guards=[], **kwargs)` | GET |
|
|
621
|
+
| `@post(path, summary, description, guards=[], **kwargs)` | POST |
|
|
622
|
+
| `@put(path, summary, description, guards=[], **kwargs)` | PUT |
|
|
623
|
+
| `@delete(path, summary, description, guards=[], **kwargs)` | DELETE |
|
|
624
|
+
| `@patch(path, summary, description, guards=[], **kwargs)` | PATCH |
|
|
528
625
|
|
|
529
626
|
Guards on a method are merged with controller-level guards — both run.
|
|
530
627
|
|
|
@@ -986,9 +1083,10 @@ Override template methods to customise behaviour:
|
|
|
986
1083
|
|---|---|---|
|
|
987
1084
|
| `get_host()` | Reads `server.host` | Hard-code or compute the bind address |
|
|
988
1085
|
| `get_port()` | Reads `server.port` | Hard-code or compute the bind port |
|
|
989
|
-
| `get_reload()` |
|
|
990
|
-
| `get_workers()` |
|
|
991
|
-
| `get_log_level()` |
|
|
1086
|
+
| `get_reload()` | FastAPI only — reads `fastapi.reload` | Force hot-reload on/off |
|
|
1087
|
+
| `get_workers()` | FastAPI only — reads `fastapi.workers` | Set worker count programmatically |
|
|
1088
|
+
| `get_log_level()` | FastAPI only — reads `fastapi.log_level` | Set log level programmatically |
|
|
1089
|
+
| `get_app_import()` | FastAPI only — reads `fastapi.app_import` | Return import string for multi-worker mode |
|
|
992
1090
|
| `get_ssl_certfile()` | Reads `server.ssl_certfile` | Load cert from a secret store |
|
|
993
1091
|
| `get_ssl_keyfile()` | Reads `server.ssl_keyfile` | Load key from a secret store |
|
|
994
1092
|
| `get_ssl_keyfile_password()` | Reads `server.ssl_keyfile_password` | Load password from a secret store |
|
|
@@ -1164,8 +1262,10 @@ Route decorators. Applied to methods inside a `@controller` class.
|
|
|
1164
1262
|
| Parameter | Type | Required | Default | Description |
|
|
1165
1263
|
|---|---|---|---|---|
|
|
1166
1264
|
| `path` | `str` | No | `""` | Route path appended to the controller prefix |
|
|
1265
|
+
| `summary` | `str` | No | `None` | Short one-line description shown in Swagger |
|
|
1266
|
+
| `description` | `str` | No | `None` | Longer description shown in Swagger |
|
|
1167
1267
|
| `guards` | `List[Callable]` | No | `[]` | Merged with controller guards |
|
|
1168
|
-
| `**kwargs` | — | No | — | Passed to the underlying FastAPI route decorator (e.g. `response_model`, `status_code
|
|
1268
|
+
| `**kwargs` | — | No | — | Passed to the underlying FastAPI route decorator (e.g. `response_model`, `status_code`) |
|
|
1169
1269
|
|
|
1170
1270
|
---
|
|
1171
1271
|
|
|
@@ -129,6 +129,52 @@ All server config lives under the `[server]` section of `config.ini`.
|
|
|
129
129
|
| `server.ssl_keyfile_password` | — | Password for encrypted private key |
|
|
130
130
|
| `api.cors_origins` | `*` | Comma-separated allowed CORS origins |
|
|
131
131
|
|
|
132
|
+
These keys apply to all `VsServer` implementations. Framework-specific config lives in its own section.
|
|
133
|
+
|
|
134
|
+
### FastAPI-specific config (`[fastapi]`)
|
|
135
|
+
|
|
136
|
+
| Key | Default | Description |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `fastapi.workers` | `1` | Number of worker processes. Requires `fastapi.app_import` to be set. |
|
|
139
|
+
| `fastapi.reload` | `false` | Enable hot reload (dev only). Requires `fastapi.app_import` to be set. |
|
|
140
|
+
| `fastapi.log_level` | `info` | Uvicorn log level (`debug`, `info`, `warning`, `error`) |
|
|
141
|
+
| `fastapi.app_import` | — | Import string for multi-worker or reload mode, e.g. `my_service.app:server`. Required when `workers > 1` or `reload = true`. |
|
|
142
|
+
|
|
143
|
+
**`config.ini` example for multi-worker production:**
|
|
144
|
+
|
|
145
|
+
```ini
|
|
146
|
+
[server]
|
|
147
|
+
name = my-service
|
|
148
|
+
version = 1.0.0
|
|
149
|
+
host = 0.0.0.0
|
|
150
|
+
port = 8000
|
|
151
|
+
docs_enabled = false
|
|
152
|
+
|
|
153
|
+
[fastapi]
|
|
154
|
+
workers = 4
|
|
155
|
+
log_level = warning
|
|
156
|
+
app_import = my_service.app:server
|
|
157
|
+
|
|
158
|
+
[api]
|
|
159
|
+
cors_origins = https://app.example.com
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**What is `app_import`?**
|
|
163
|
+
|
|
164
|
+
When running multiple workers, uvicorn forks child processes that each need to reconstruct the app independently. They can't share the in-memory object from the parent process — they need an import path to build their own copy.
|
|
165
|
+
|
|
166
|
+
`app_import` is `module.path:variable_name` where `variable_name` is whatever you assigned `VsServerFactory.get()` to:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# my_service/app.py
|
|
170
|
+
server = VsServerFactory.get("fastapi", config) # → app_import = my_service.app:server
|
|
171
|
+
server.run()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If `app_import` is not set, vs-server will attempt to auto-detect it and log it at startup. Copy the logged value into your config to make it explicit.
|
|
175
|
+
|
|
176
|
+
**Custom server implementations** follow the same pattern — add a `[my_framework]` section in `config.ini` for any framework-specific keys and read them in your `VsServer` subclass.
|
|
177
|
+
|
|
132
178
|
**`config.ini` example:**
|
|
133
179
|
|
|
134
180
|
```ini
|
|
@@ -250,6 +296,57 @@ async def get_report():
|
|
|
250
296
|
|
|
251
297
|
---
|
|
252
298
|
|
|
299
|
+
## Request and Response Models
|
|
300
|
+
|
|
301
|
+
All request bodies and response models must be Pydantic models — not SQLAlchemy entities or plain dicts. This is a hard requirement of FastAPI and applies to all server implementations built on vs-server.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from pydantic import BaseModel
|
|
305
|
+
|
|
306
|
+
class UserRequest(BaseModel):
|
|
307
|
+
name: str
|
|
308
|
+
email: str
|
|
309
|
+
|
|
310
|
+
class UserResponse(BaseModel):
|
|
311
|
+
id: str
|
|
312
|
+
name: str
|
|
313
|
+
email: str
|
|
314
|
+
|
|
315
|
+
model_config = {"from_attributes": True}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The mapping between your SQLAlchemy entity and your Pydantic schema is your responsibility:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
Request (Pydantic) → Service → Entity (SQLAlchemy) → DB
|
|
322
|
+
DB → Entity (SQLAlchemy) → Service → Response (Pydantic)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Never pass a SQLAlchemy entity as `response_model` — FastAPI will reject it at startup. Never return a SQLAlchemy entity directly from a route — serialize it first.
|
|
326
|
+
|
|
327
|
+
Keeping schemas separate from entities is intentional: the DB schema and API schema can evolve independently, and sensitive fields (e.g. `password`) are excluded from responses by default.
|
|
328
|
+
|
|
329
|
+
### @model — Reduce Schema Boilerplate (requires vs-db)
|
|
330
|
+
|
|
331
|
+
If you are using `vs-db`, the `@model` decorator derives Pydantic fields directly from a SQLAlchemy entity so you don't repeat type definitions. Nullable columns become `Optional[T] = None` automatically. Fields already defined on the class are never overridden.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from pydantic import BaseModel, Field
|
|
335
|
+
from vs_db.decorator.vs_model_decorator import model
|
|
336
|
+
|
|
337
|
+
@model(AppUser, exclude=["password"])
|
|
338
|
+
class UserResponse(BaseModel):
|
|
339
|
+
pass # all AppUser fields injected except password
|
|
340
|
+
|
|
341
|
+
@model(AppUser, include=["email", "first_name", "last_name"])
|
|
342
|
+
class CreateUserRequest(BaseModel):
|
|
343
|
+
first_name: str = Field(min_length=1) # user-defined — decorator skips it
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
See the `vs-db` README for full documentation on `@model`.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
253
350
|
## HTTP Controllers
|
|
254
351
|
|
|
255
352
|
Annotate a class with `@controller` and methods with HTTP verb decorators.
|
|
@@ -301,11 +398,11 @@ All FastAPI route parameters (`response_model`, `status_code`, `tags`, `summary`
|
|
|
301
398
|
|
|
302
399
|
| Decorator | HTTP Method |
|
|
303
400
|
|---|---|
|
|
304
|
-
| `@get(path, guards=[], **kwargs)` | GET |
|
|
305
|
-
| `@post(path, guards=[], **kwargs)` | POST |
|
|
306
|
-
| `@put(path, guards=[], **kwargs)` | PUT |
|
|
307
|
-
| `@delete(path, guards=[], **kwargs)` | DELETE |
|
|
308
|
-
| `@patch(path, guards=[], **kwargs)` | PATCH |
|
|
401
|
+
| `@get(path, summary, description, guards=[], **kwargs)` | GET |
|
|
402
|
+
| `@post(path, summary, description, guards=[], **kwargs)` | POST |
|
|
403
|
+
| `@put(path, summary, description, guards=[], **kwargs)` | PUT |
|
|
404
|
+
| `@delete(path, summary, description, guards=[], **kwargs)` | DELETE |
|
|
405
|
+
| `@patch(path, summary, description, guards=[], **kwargs)` | PATCH |
|
|
309
406
|
|
|
310
407
|
Guards on a method are merged with controller-level guards — both run.
|
|
311
408
|
|
|
@@ -767,9 +864,10 @@ Override template methods to customise behaviour:
|
|
|
767
864
|
|---|---|---|
|
|
768
865
|
| `get_host()` | Reads `server.host` | Hard-code or compute the bind address |
|
|
769
866
|
| `get_port()` | Reads `server.port` | Hard-code or compute the bind port |
|
|
770
|
-
| `get_reload()` |
|
|
771
|
-
| `get_workers()` |
|
|
772
|
-
| `get_log_level()` |
|
|
867
|
+
| `get_reload()` | FastAPI only — reads `fastapi.reload` | Force hot-reload on/off |
|
|
868
|
+
| `get_workers()` | FastAPI only — reads `fastapi.workers` | Set worker count programmatically |
|
|
869
|
+
| `get_log_level()` | FastAPI only — reads `fastapi.log_level` | Set log level programmatically |
|
|
870
|
+
| `get_app_import()` | FastAPI only — reads `fastapi.app_import` | Return import string for multi-worker mode |
|
|
773
871
|
| `get_ssl_certfile()` | Reads `server.ssl_certfile` | Load cert from a secret store |
|
|
774
872
|
| `get_ssl_keyfile()` | Reads `server.ssl_keyfile` | Load key from a secret store |
|
|
775
873
|
| `get_ssl_keyfile_password()` | Reads `server.ssl_keyfile_password` | Load password from a secret store |
|
|
@@ -945,8 +1043,10 @@ Route decorators. Applied to methods inside a `@controller` class.
|
|
|
945
1043
|
| Parameter | Type | Required | Default | Description |
|
|
946
1044
|
|---|---|---|---|---|
|
|
947
1045
|
| `path` | `str` | No | `""` | Route path appended to the controller prefix |
|
|
1046
|
+
| `summary` | `str` | No | `None` | Short one-line description shown in Swagger |
|
|
1047
|
+
| `description` | `str` | No | `None` | Longer description shown in Swagger |
|
|
948
1048
|
| `guards` | `List[Callable]` | No | `[]` | Merged with controller guards |
|
|
949
|
-
| `**kwargs` | — | No | — | Passed to the underlying FastAPI route decorator (e.g. `response_model`, `status_code
|
|
1049
|
+
| `**kwargs` | — | No | — | Passed to the underlying FastAPI route decorator (e.g. `response_model`, `status_code`) |
|
|
950
1050
|
|
|
951
1051
|
---
|
|
952
1052
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vs-server"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "HTTP server abstraction for Viveka Sutra — FastAPI wrapper with lifecycle, routing, and middleware"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Callable, List, Type
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def controller(path: str, description: str = "", guards: List[Callable] = None):
|
|
5
|
+
def decorator(cls: Type) -> Type:
|
|
6
|
+
cls._vs_controller = {
|
|
7
|
+
"path": path,
|
|
8
|
+
"description": description,
|
|
9
|
+
"guards": guards or [],
|
|
10
|
+
}
|
|
11
|
+
return cls
|
|
12
|
+
return decorator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _route(method: str, path: str = "", guards: List[Callable] = None, summary: str = None, description: str = None, **kwargs):
|
|
16
|
+
def decorator(fn: Callable) -> Callable:
|
|
17
|
+
fn._vs_route = {
|
|
18
|
+
"method": method,
|
|
19
|
+
"path": path,
|
|
20
|
+
"guards": guards or [],
|
|
21
|
+
"kwargs": {
|
|
22
|
+
**({"summary": summary} if summary else {}),
|
|
23
|
+
**({"description": description} if description else {}),
|
|
24
|
+
**kwargs,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
return fn
|
|
28
|
+
return decorator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get(path: str = "", guards: List[Callable] = None, summary: str = None, description: str = None, **kwargs):
|
|
32
|
+
return _route("GET", path, guards=guards, summary=summary, description=description, **kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def post(path: str = "", guards: List[Callable] = None, summary: str = None, description: str = None, **kwargs):
|
|
36
|
+
return _route("POST", path, guards=guards, summary=summary, description=description, **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def put(path: str = "", guards: List[Callable] = None, summary: str = None, description: str = None, **kwargs):
|
|
40
|
+
return _route("PUT", path, guards=guards, summary=summary, description=description, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def delete(path: str = "", guards: List[Callable] = None, summary: str = None, description: str = None, **kwargs):
|
|
44
|
+
return _route("DELETE", path, guards=guards, summary=summary, description=description, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def patch(path: str = "", guards: List[Callable] = None, summary: str = None, description: str = None, **kwargs):
|
|
48
|
+
return _route("PATCH", path, guards=guards, summary=summary, description=description, **kwargs)
|
|
@@ -167,6 +167,18 @@ class VsFastApiServer(VsServer):
|
|
|
167
167
|
def get_app(self):
|
|
168
168
|
return self._app
|
|
169
169
|
|
|
170
|
+
def get_reload(self) -> bool:
|
|
171
|
+
return self._config.get("fastapi.reload", False, bool)
|
|
172
|
+
|
|
173
|
+
def get_workers(self) -> int:
|
|
174
|
+
return self._config.get("fastapi.workers", 1, int)
|
|
175
|
+
|
|
176
|
+
def get_log_level(self) -> str:
|
|
177
|
+
return self._config.get("fastapi.log_level", "info").lower()
|
|
178
|
+
|
|
179
|
+
def get_app_import(self) -> Optional[str]:
|
|
180
|
+
return self._config.get("fastapi.app_import")
|
|
181
|
+
|
|
170
182
|
def run(self) -> None:
|
|
171
183
|
import uvicorn
|
|
172
184
|
host = self.get_host()
|
|
@@ -174,14 +186,19 @@ class VsFastApiServer(VsServer):
|
|
|
174
186
|
reload = self.get_reload()
|
|
175
187
|
workers = self.get_workers()
|
|
176
188
|
log_level = self.get_log_level()
|
|
189
|
+
app_import = self.get_app_import()
|
|
177
190
|
ssl_certfile = self.get_ssl_certfile()
|
|
178
191
|
ssl_keyfile = self.get_ssl_keyfile()
|
|
179
192
|
ssl_keyfile_password = self.get_ssl_keyfile_password()
|
|
180
193
|
|
|
181
|
-
self._print_banner(host, port, reload, tls=bool(ssl_certfile and ssl_keyfile))
|
|
194
|
+
self._print_banner(host, port, reload, workers, tls=bool(ssl_certfile and ssl_keyfile))
|
|
195
|
+
|
|
196
|
+
use_import = workers > 1 or reload
|
|
197
|
+
if use_import and not app_import:
|
|
198
|
+
app_import = self._detect_app_import()
|
|
182
199
|
|
|
183
200
|
uvicorn_config = {
|
|
184
|
-
"app": self._app,
|
|
201
|
+
"app": app_import if use_import and app_import else self._app,
|
|
185
202
|
"host": host,
|
|
186
203
|
"port": port,
|
|
187
204
|
"log_level": log_level,
|
|
@@ -189,7 +206,13 @@ class VsFastApiServer(VsServer):
|
|
|
189
206
|
if reload:
|
|
190
207
|
uvicorn_config["reload"] = True
|
|
191
208
|
elif workers > 1:
|
|
192
|
-
|
|
209
|
+
if app_import:
|
|
210
|
+
uvicorn_config["workers"] = workers
|
|
211
|
+
else:
|
|
212
|
+
self._logger.warning(
|
|
213
|
+
"fastapi.workers > 1 requires fastapi.app_import to be set in config.ini. "
|
|
214
|
+
"Falling back to 1 worker."
|
|
215
|
+
)
|
|
193
216
|
|
|
194
217
|
if ssl_certfile and ssl_keyfile:
|
|
195
218
|
uvicorn_config["ssl_certfile"] = ssl_certfile
|
|
@@ -199,6 +222,21 @@ class VsFastApiServer(VsServer):
|
|
|
199
222
|
|
|
200
223
|
uvicorn.run(**uvicorn_config)
|
|
201
224
|
|
|
225
|
+
def _detect_app_import(self) -> Optional[str]:
|
|
226
|
+
try:
|
|
227
|
+
import __main__
|
|
228
|
+
module = __main__.__spec__.name if hasattr(__main__, "__spec__") and __main__.__spec__ else None
|
|
229
|
+
if not module:
|
|
230
|
+
return None
|
|
231
|
+
for var_name, val in vars(__main__).items():
|
|
232
|
+
if val is self._app:
|
|
233
|
+
detected = f"{module}:{var_name}"
|
|
234
|
+
self._logger.info(f"Detected app_import = {detected} (set fastapi.app_import in config.ini to make this explicit)")
|
|
235
|
+
return detected
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
return None
|
|
239
|
+
|
|
202
240
|
# ------------------------------------------------------------------
|
|
203
241
|
# Internal
|
|
204
242
|
# ------------------------------------------------------------------
|
|
@@ -317,16 +355,17 @@ class VsFastApiServer(VsServer):
|
|
|
317
355
|
)(fn)
|
|
318
356
|
app.include_router(router)
|
|
319
357
|
|
|
320
|
-
def _print_banner(self, host: str, port: int, reload: bool, tls: bool = False) -> None:
|
|
358
|
+
def _print_banner(self, host: str, port: int, reload: bool, workers: int = 1, tls: bool = False) -> None:
|
|
321
359
|
mode = "Development (reload)" if reload else "Production"
|
|
322
360
|
scheme = "https" if tls else "http"
|
|
323
361
|
self._logger.info("=" * 50)
|
|
324
362
|
self._logger.info(f" {self.name} v{self.version}")
|
|
325
363
|
self._logger.info("=" * 50)
|
|
326
|
-
self._logger.info(f" Host
|
|
327
|
-
self._logger.info(f" Port
|
|
328
|
-
self._logger.info(f" Mode
|
|
329
|
-
self._logger.info(f"
|
|
364
|
+
self._logger.info(f" Host : {host}")
|
|
365
|
+
self._logger.info(f" Port : {port}")
|
|
366
|
+
self._logger.info(f" Mode : {mode}")
|
|
367
|
+
self._logger.info(f" Workers : {workers}")
|
|
368
|
+
self._logger.info(f" TLS : {'enabled' if tls else 'disabled'}")
|
|
330
369
|
if self.is_docs_enabled() and self.get_docs_url():
|
|
331
370
|
self._logger.info(f" Docs : {scheme}://{host}:{port}{self.get_docs_url()}")
|
|
332
371
|
self._logger.info(f" WS/SSE : {scheme}://{host}:{port}/vs-docs")
|
|
@@ -48,15 +48,6 @@ class VsServer(ABC):
|
|
|
48
48
|
def get_port(self) -> int:
|
|
49
49
|
return self._config.get("server.port", 8000, int)
|
|
50
50
|
|
|
51
|
-
def get_reload(self) -> bool:
|
|
52
|
-
return self._config.get("server.reload", False, bool)
|
|
53
|
-
|
|
54
|
-
def get_workers(self) -> int:
|
|
55
|
-
return self._config.get("server.workers", 1, int)
|
|
56
|
-
|
|
57
|
-
def get_log_level(self) -> str:
|
|
58
|
-
return self._config.get("server.log_level", "info").lower()
|
|
59
|
-
|
|
60
51
|
def get_ssl_certfile(self) -> Optional[str]:
|
|
61
52
|
return self._config.get("server.ssl_certfile")
|
|
62
53
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vs-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: HTTP server abstraction for Viveka Sutra — FastAPI wrapper with lifecycle, routing, and middleware
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -348,6 +348,52 @@ All server config lives under the `[server]` section of `config.ini`.
|
|
|
348
348
|
| `server.ssl_keyfile_password` | — | Password for encrypted private key |
|
|
349
349
|
| `api.cors_origins` | `*` | Comma-separated allowed CORS origins |
|
|
350
350
|
|
|
351
|
+
These keys apply to all `VsServer` implementations. Framework-specific config lives in its own section.
|
|
352
|
+
|
|
353
|
+
### FastAPI-specific config (`[fastapi]`)
|
|
354
|
+
|
|
355
|
+
| Key | Default | Description |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `fastapi.workers` | `1` | Number of worker processes. Requires `fastapi.app_import` to be set. |
|
|
358
|
+
| `fastapi.reload` | `false` | Enable hot reload (dev only). Requires `fastapi.app_import` to be set. |
|
|
359
|
+
| `fastapi.log_level` | `info` | Uvicorn log level (`debug`, `info`, `warning`, `error`) |
|
|
360
|
+
| `fastapi.app_import` | — | Import string for multi-worker or reload mode, e.g. `my_service.app:server`. Required when `workers > 1` or `reload = true`. |
|
|
361
|
+
|
|
362
|
+
**`config.ini` example for multi-worker production:**
|
|
363
|
+
|
|
364
|
+
```ini
|
|
365
|
+
[server]
|
|
366
|
+
name = my-service
|
|
367
|
+
version = 1.0.0
|
|
368
|
+
host = 0.0.0.0
|
|
369
|
+
port = 8000
|
|
370
|
+
docs_enabled = false
|
|
371
|
+
|
|
372
|
+
[fastapi]
|
|
373
|
+
workers = 4
|
|
374
|
+
log_level = warning
|
|
375
|
+
app_import = my_service.app:server
|
|
376
|
+
|
|
377
|
+
[api]
|
|
378
|
+
cors_origins = https://app.example.com
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**What is `app_import`?**
|
|
382
|
+
|
|
383
|
+
When running multiple workers, uvicorn forks child processes that each need to reconstruct the app independently. They can't share the in-memory object from the parent process — they need an import path to build their own copy.
|
|
384
|
+
|
|
385
|
+
`app_import` is `module.path:variable_name` where `variable_name` is whatever you assigned `VsServerFactory.get()` to:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
# my_service/app.py
|
|
389
|
+
server = VsServerFactory.get("fastapi", config) # → app_import = my_service.app:server
|
|
390
|
+
server.run()
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
If `app_import` is not set, vs-server will attempt to auto-detect it and log it at startup. Copy the logged value into your config to make it explicit.
|
|
394
|
+
|
|
395
|
+
**Custom server implementations** follow the same pattern — add a `[my_framework]` section in `config.ini` for any framework-specific keys and read them in your `VsServer` subclass.
|
|
396
|
+
|
|
351
397
|
**`config.ini` example:**
|
|
352
398
|
|
|
353
399
|
```ini
|
|
@@ -469,6 +515,57 @@ async def get_report():
|
|
|
469
515
|
|
|
470
516
|
---
|
|
471
517
|
|
|
518
|
+
## Request and Response Models
|
|
519
|
+
|
|
520
|
+
All request bodies and response models must be Pydantic models — not SQLAlchemy entities or plain dicts. This is a hard requirement of FastAPI and applies to all server implementations built on vs-server.
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
from pydantic import BaseModel
|
|
524
|
+
|
|
525
|
+
class UserRequest(BaseModel):
|
|
526
|
+
name: str
|
|
527
|
+
email: str
|
|
528
|
+
|
|
529
|
+
class UserResponse(BaseModel):
|
|
530
|
+
id: str
|
|
531
|
+
name: str
|
|
532
|
+
email: str
|
|
533
|
+
|
|
534
|
+
model_config = {"from_attributes": True}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The mapping between your SQLAlchemy entity and your Pydantic schema is your responsibility:
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
Request (Pydantic) → Service → Entity (SQLAlchemy) → DB
|
|
541
|
+
DB → Entity (SQLAlchemy) → Service → Response (Pydantic)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Never pass a SQLAlchemy entity as `response_model` — FastAPI will reject it at startup. Never return a SQLAlchemy entity directly from a route — serialize it first.
|
|
545
|
+
|
|
546
|
+
Keeping schemas separate from entities is intentional: the DB schema and API schema can evolve independently, and sensitive fields (e.g. `password`) are excluded from responses by default.
|
|
547
|
+
|
|
548
|
+
### @model — Reduce Schema Boilerplate (requires vs-db)
|
|
549
|
+
|
|
550
|
+
If you are using `vs-db`, the `@model` decorator derives Pydantic fields directly from a SQLAlchemy entity so you don't repeat type definitions. Nullable columns become `Optional[T] = None` automatically. Fields already defined on the class are never overridden.
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
from pydantic import BaseModel, Field
|
|
554
|
+
from vs_db.decorator.vs_model_decorator import model
|
|
555
|
+
|
|
556
|
+
@model(AppUser, exclude=["password"])
|
|
557
|
+
class UserResponse(BaseModel):
|
|
558
|
+
pass # all AppUser fields injected except password
|
|
559
|
+
|
|
560
|
+
@model(AppUser, include=["email", "first_name", "last_name"])
|
|
561
|
+
class CreateUserRequest(BaseModel):
|
|
562
|
+
first_name: str = Field(min_length=1) # user-defined — decorator skips it
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
See the `vs-db` README for full documentation on `@model`.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
472
569
|
## HTTP Controllers
|
|
473
570
|
|
|
474
571
|
Annotate a class with `@controller` and methods with HTTP verb decorators.
|
|
@@ -520,11 +617,11 @@ All FastAPI route parameters (`response_model`, `status_code`, `tags`, `summary`
|
|
|
520
617
|
|
|
521
618
|
| Decorator | HTTP Method |
|
|
522
619
|
|---|---|
|
|
523
|
-
| `@get(path, guards=[], **kwargs)` | GET |
|
|
524
|
-
| `@post(path, guards=[], **kwargs)` | POST |
|
|
525
|
-
| `@put(path, guards=[], **kwargs)` | PUT |
|
|
526
|
-
| `@delete(path, guards=[], **kwargs)` | DELETE |
|
|
527
|
-
| `@patch(path, guards=[], **kwargs)` | PATCH |
|
|
620
|
+
| `@get(path, summary, description, guards=[], **kwargs)` | GET |
|
|
621
|
+
| `@post(path, summary, description, guards=[], **kwargs)` | POST |
|
|
622
|
+
| `@put(path, summary, description, guards=[], **kwargs)` | PUT |
|
|
623
|
+
| `@delete(path, summary, description, guards=[], **kwargs)` | DELETE |
|
|
624
|
+
| `@patch(path, summary, description, guards=[], **kwargs)` | PATCH |
|
|
528
625
|
|
|
529
626
|
Guards on a method are merged with controller-level guards — both run.
|
|
530
627
|
|
|
@@ -986,9 +1083,10 @@ Override template methods to customise behaviour:
|
|
|
986
1083
|
|---|---|---|
|
|
987
1084
|
| `get_host()` | Reads `server.host` | Hard-code or compute the bind address |
|
|
988
1085
|
| `get_port()` | Reads `server.port` | Hard-code or compute the bind port |
|
|
989
|
-
| `get_reload()` |
|
|
990
|
-
| `get_workers()` |
|
|
991
|
-
| `get_log_level()` |
|
|
1086
|
+
| `get_reload()` | FastAPI only — reads `fastapi.reload` | Force hot-reload on/off |
|
|
1087
|
+
| `get_workers()` | FastAPI only — reads `fastapi.workers` | Set worker count programmatically |
|
|
1088
|
+
| `get_log_level()` | FastAPI only — reads `fastapi.log_level` | Set log level programmatically |
|
|
1089
|
+
| `get_app_import()` | FastAPI only — reads `fastapi.app_import` | Return import string for multi-worker mode |
|
|
992
1090
|
| `get_ssl_certfile()` | Reads `server.ssl_certfile` | Load cert from a secret store |
|
|
993
1091
|
| `get_ssl_keyfile()` | Reads `server.ssl_keyfile` | Load key from a secret store |
|
|
994
1092
|
| `get_ssl_keyfile_password()` | Reads `server.ssl_keyfile_password` | Load password from a secret store |
|
|
@@ -1164,8 +1262,10 @@ Route decorators. Applied to methods inside a `@controller` class.
|
|
|
1164
1262
|
| Parameter | Type | Required | Default | Description |
|
|
1165
1263
|
|---|---|---|---|---|
|
|
1166
1264
|
| `path` | `str` | No | `""` | Route path appended to the controller prefix |
|
|
1265
|
+
| `summary` | `str` | No | `None` | Short one-line description shown in Swagger |
|
|
1266
|
+
| `description` | `str` | No | `None` | Longer description shown in Swagger |
|
|
1167
1267
|
| `guards` | `List[Callable]` | No | `[]` | Merged with controller guards |
|
|
1168
|
-
| `**kwargs` | — | No | — | Passed to the underlying FastAPI route decorator (e.g. `response_model`, `status_code
|
|
1268
|
+
| `**kwargs` | — | No | — | Passed to the underlying FastAPI route decorator (e.g. `response_model`, `status_code`) |
|
|
1169
1269
|
|
|
1170
1270
|
---
|
|
1171
1271
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from typing import Callable, List, Type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def controller(path: str, description: str = "", guards: List[Callable] = None):
|
|
5
|
-
def decorator(cls: Type) -> Type:
|
|
6
|
-
cls._vs_controller = {
|
|
7
|
-
"path": path,
|
|
8
|
-
"description": description,
|
|
9
|
-
"guards": guards or [],
|
|
10
|
-
}
|
|
11
|
-
return cls
|
|
12
|
-
return decorator
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _route(method: str, path: str = "", guards: List[Callable] = None, **kwargs):
|
|
16
|
-
def decorator(fn: Callable) -> Callable:
|
|
17
|
-
fn._vs_route = {
|
|
18
|
-
"method": method,
|
|
19
|
-
"path": path,
|
|
20
|
-
"guards": guards or [],
|
|
21
|
-
"kwargs": kwargs,
|
|
22
|
-
}
|
|
23
|
-
return fn
|
|
24
|
-
return decorator
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get(path: str = "", guards: List[Callable] = None, **kwargs):
|
|
28
|
-
return _route("GET", path, guards=guards, **kwargs)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def post(path: str = "", guards: List[Callable] = None, **kwargs):
|
|
32
|
-
return _route("POST", path, guards=guards, **kwargs)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def put(path: str = "", guards: List[Callable] = None, **kwargs):
|
|
36
|
-
return _route("PUT", path, guards=guards, **kwargs)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def delete(path: str = "", guards: List[Callable] = None, **kwargs):
|
|
40
|
-
return _route("DELETE", path, guards=guards, **kwargs)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def patch(path: str = "", guards: List[Callable] = None, **kwargs):
|
|
44
|
-
return _route("PATCH", path, guards=guards, **kwargs)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|