qivo 0.1.1__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.
- qivo-0.1.1/PKG-INFO +199 -0
- qivo-0.1.1/README.md +189 -0
- qivo-0.1.1/pyproject.toml +18 -0
- qivo-0.1.1/pyproject.toml.orig +18 -0
- qivo-0.1.1/src/qivo/__init__.py +83 -0
- qivo-0.1.1/src/qivo/cli/__init__.py +224 -0
- qivo-0.1.1/src/qivo/cli/init.py +243 -0
- qivo-0.1.1/src/qivo/db/__init__.py +0 -0
- qivo-0.1.1/src/qivo/db/sql/__init__.py +119 -0
- qivo-0.1.1/src/qivo/db/sql/extensions.py +50 -0
- qivo-0.1.1/src/qivo/db/sql/migrations.py +145 -0
- qivo-0.1.1/src/qivo/defaults.py +0 -0
- qivo-0.1.1/src/qivo/dependencies.py +133 -0
- qivo-0.1.1/src/qivo/extensions.py +15 -0
- qivo-0.1.1/src/qivo/guards.py +76 -0
- qivo-0.1.1/src/qivo/serialization.py +148 -0
qivo-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: qivo
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A small Flask layer for dependency injection, authorization, and response serialization.
|
|
5
|
+
Requires-Dist: alembic>=1.20.0
|
|
6
|
+
Requires-Dist: flask>=3.1.3
|
|
7
|
+
Requires-Dist: sqlalchemy>=2.1.1
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Qivo
|
|
12
|
+
|
|
13
|
+
Qivo is a small Flask layer for dependency injection, authorization, and response
|
|
14
|
+
serialization. Each app owns its container, serializer, guards, and view-extension
|
|
15
|
+
pipeline; it does not choose or depend on a database.
|
|
16
|
+
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
Initialize a suggested app layout in the current directory, or choose a target
|
|
20
|
+
directory and Python package name:
|
|
21
|
+
|
|
22
|
+
```console
|
|
23
|
+
uv run qivo init
|
|
24
|
+
uv run qivo init --name "Mi lista" --path ./service --package service
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`qivo init` always asks for a required application name unless `--name` is passed.
|
|
28
|
+
The scaffold contains a minimal Flask todo list with a `Task` model, the four CRUD
|
|
29
|
+
endpoints, `qivo.toml`, and one HTML template at `app/template/app.html`, loaded
|
|
30
|
+
with Flask's normal `render_template` mechanism. Existing files are left untouched.
|
|
31
|
+
Edit the TOML settings to configure the database URL, model modules, model base,
|
|
32
|
+
and migrations directory.
|
|
33
|
+
|
|
34
|
+
```console
|
|
35
|
+
uv run qivo migrate --message "initial schema"
|
|
36
|
+
uv run qivo migrate:apply
|
|
37
|
+
uv run qivo migrate:revert
|
|
38
|
+
uv run qivo migrate:revert base
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Migration commands accept `--config`, `--database-url`, `--model-base`, repeated
|
|
42
|
+
`--models`, and `--migrations-dir` overrides. `migrate` also supports `--empty`;
|
|
43
|
+
`migrate:apply` defaults to `head`, and `migrate:revert` defaults to `-1`.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from flask import Flask
|
|
47
|
+
|
|
48
|
+
from qivo import Qivo
|
|
49
|
+
|
|
50
|
+
app = Flask(__name__)
|
|
51
|
+
qivo = Qivo(app)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Greeting:
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.message = "Hello"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
qivo.container.register(Greeting)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.route("/greeting", methods=["GET"])
|
|
63
|
+
@qivo.view()
|
|
64
|
+
def greeting(service: Greeting):
|
|
65
|
+
return {"message": service.message}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Authorization
|
|
69
|
+
|
|
70
|
+
Implement `Authenticator` against the identity source used by your application,
|
|
71
|
+
then add it to the app's `guards` mapping. Apply authentication or policies per
|
|
72
|
+
route with `auth`, `guard`, and `policies`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from qivo.guards import Authenticator, WithAll
|
|
76
|
+
|
|
77
|
+
qivo.guards["web"] = MyAuthenticator()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.route("/admin", methods=["GET"])
|
|
81
|
+
@qivo.view(auth=True, policies=[WithAll(["admin:read"])])
|
|
82
|
+
def admin_view():
|
|
83
|
+
return {"ok": True}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Extending Views
|
|
87
|
+
|
|
88
|
+
Add a `ViewExtension` to customize view handling without changing `Qivo` or the
|
|
89
|
+
built-in features. Extensions are applied outside-in in registration order. Pass
|
|
90
|
+
extension-specific route settings directly to `qivo.view(...)`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from functools import wraps
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AuditExtension:
|
|
97
|
+
def wrap_view(self, app, view_func, *, options):
|
|
98
|
+
@wraps(view_func)
|
|
99
|
+
def wrapper(*args, **kwargs):
|
|
100
|
+
result = view_func(*args, **kwargs)
|
|
101
|
+
app.app.logger.info("audit category=%s", options.get("audit_category"))
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
return wrapper
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
qivo.register_view_extension(AuditExtension())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.route("/records", methods=["GET"])
|
|
111
|
+
@qivo.view(audit_category="records")
|
|
112
|
+
def records():
|
|
113
|
+
return []
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
An extension receives the Qivo instance, the next view callable, and an immutable
|
|
117
|
+
mapping of Qivo route options. Register extensions before declaring routes so they
|
|
118
|
+
wrap those routes. Place `@qivo.view(...)` directly below either `@app.route(...)`
|
|
119
|
+
or `@blueprint.route(...)`; Flask handles registration and lifecycle as usual.
|
|
120
|
+
|
|
121
|
+
## SQLAlchemy
|
|
122
|
+
|
|
123
|
+
Configure the database declaratively in Flask, then attach the SQL extension.
|
|
124
|
+
`SQLEngine` reads the URL, engine options, and session options from `app.config`
|
|
125
|
+
and configures the model base automatically. Each terminal query opens and closes
|
|
126
|
+
its own session:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from flask import Flask
|
|
130
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
131
|
+
|
|
132
|
+
from qivo import Qivo
|
|
133
|
+
from qivo.db.sql import Model
|
|
134
|
+
from qivo.db.sql.extensions import SQLEngine
|
|
135
|
+
|
|
136
|
+
app = Flask(__name__)
|
|
137
|
+
app.config.from_mapping(
|
|
138
|
+
SQLALCHEMY_DATABASE_URI="sqlite:///app.db",
|
|
139
|
+
SQLALCHEMY_ENGINE_OPTIONS={},
|
|
140
|
+
SQLALCHEMY_SESSION_OPTIONS={},
|
|
141
|
+
)
|
|
142
|
+
qivo = Qivo(app)
|
|
143
|
+
db = SQLEngine(app)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class User(Model):
|
|
147
|
+
__tablename__ = "users"
|
|
148
|
+
|
|
149
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
150
|
+
name: Mapped[str]
|
|
151
|
+
is_active: Mapped[bool]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
user = User(name="admin", is_active=True)
|
|
155
|
+
user.q.save()
|
|
156
|
+
|
|
157
|
+
user = User.q.get(user.id)
|
|
158
|
+
user.name = "root"
|
|
159
|
+
user.q.save()
|
|
160
|
+
|
|
161
|
+
user.q.delete() # Returns False if the row no longer exists.
|
|
162
|
+
|
|
163
|
+
admin = User.q.filter(name="admin").first()
|
|
164
|
+
active_admins = User.q.where(User.name == "admin").where(
|
|
165
|
+
User.is_active == True
|
|
166
|
+
).all()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use eager loading for relationships that must be accessed after a query returns,
|
|
170
|
+
because its session is closed when the operation completes. `SQLEngine` does not
|
|
171
|
+
create tables automatically; use the migration commands for schema changes.
|
|
172
|
+
|
|
173
|
+
## Migrations
|
|
174
|
+
|
|
175
|
+
`AlembicMigrations` uses `Model.metadata` and creates a standard `migrations/`
|
|
176
|
+
directory on the first revision. Import all model modules before autogenerating
|
|
177
|
+
so Alembic can see their tables:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from qivo.db.sql import AlembicMigrations
|
|
181
|
+
|
|
182
|
+
migrations = AlembicMigrations(engine)
|
|
183
|
+
migrations.revision("initial schema")
|
|
184
|
+
migrations.upgrade()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The directory and autogeneration options can be customized:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from qivo.db.sql import AlembicMigrations, MigrationConfig
|
|
191
|
+
|
|
192
|
+
migrations = AlembicMigrations(
|
|
193
|
+
engine,
|
|
194
|
+
config=MigrationConfig(directory="db/migrations", compare_type=True),
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use `migrations.downgrade()` to revert one revision or pass a target such as
|
|
199
|
+
`"base"`; use `migrations.stamp()` to mark a database without applying scripts.
|
qivo-0.1.1/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Qivo
|
|
2
|
+
|
|
3
|
+
Qivo is a small Flask layer for dependency injection, authorization, and response
|
|
4
|
+
serialization. Each app owns its container, serializer, guards, and view-extension
|
|
5
|
+
pipeline; it does not choose or depend on a database.
|
|
6
|
+
|
|
7
|
+
## CLI
|
|
8
|
+
|
|
9
|
+
Initialize a suggested app layout in the current directory, or choose a target
|
|
10
|
+
directory and Python package name:
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
uv run qivo init
|
|
14
|
+
uv run qivo init --name "Mi lista" --path ./service --package service
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`qivo init` always asks for a required application name unless `--name` is passed.
|
|
18
|
+
The scaffold contains a minimal Flask todo list with a `Task` model, the four CRUD
|
|
19
|
+
endpoints, `qivo.toml`, and one HTML template at `app/template/app.html`, loaded
|
|
20
|
+
with Flask's normal `render_template` mechanism. Existing files are left untouched.
|
|
21
|
+
Edit the TOML settings to configure the database URL, model modules, model base,
|
|
22
|
+
and migrations directory.
|
|
23
|
+
|
|
24
|
+
```console
|
|
25
|
+
uv run qivo migrate --message "initial schema"
|
|
26
|
+
uv run qivo migrate:apply
|
|
27
|
+
uv run qivo migrate:revert
|
|
28
|
+
uv run qivo migrate:revert base
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Migration commands accept `--config`, `--database-url`, `--model-base`, repeated
|
|
32
|
+
`--models`, and `--migrations-dir` overrides. `migrate` also supports `--empty`;
|
|
33
|
+
`migrate:apply` defaults to `head`, and `migrate:revert` defaults to `-1`.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from flask import Flask
|
|
37
|
+
|
|
38
|
+
from qivo import Qivo
|
|
39
|
+
|
|
40
|
+
app = Flask(__name__)
|
|
41
|
+
qivo = Qivo(app)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Greeting:
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self.message = "Hello"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
qivo.container.register(Greeting)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.route("/greeting", methods=["GET"])
|
|
53
|
+
@qivo.view()
|
|
54
|
+
def greeting(service: Greeting):
|
|
55
|
+
return {"message": service.message}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Authorization
|
|
59
|
+
|
|
60
|
+
Implement `Authenticator` against the identity source used by your application,
|
|
61
|
+
then add it to the app's `guards` mapping. Apply authentication or policies per
|
|
62
|
+
route with `auth`, `guard`, and `policies`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from qivo.guards import Authenticator, WithAll
|
|
66
|
+
|
|
67
|
+
qivo.guards["web"] = MyAuthenticator()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.route("/admin", methods=["GET"])
|
|
71
|
+
@qivo.view(auth=True, policies=[WithAll(["admin:read"])])
|
|
72
|
+
def admin_view():
|
|
73
|
+
return {"ok": True}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Extending Views
|
|
77
|
+
|
|
78
|
+
Add a `ViewExtension` to customize view handling without changing `Qivo` or the
|
|
79
|
+
built-in features. Extensions are applied outside-in in registration order. Pass
|
|
80
|
+
extension-specific route settings directly to `qivo.view(...)`:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from functools import wraps
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AuditExtension:
|
|
87
|
+
def wrap_view(self, app, view_func, *, options):
|
|
88
|
+
@wraps(view_func)
|
|
89
|
+
def wrapper(*args, **kwargs):
|
|
90
|
+
result = view_func(*args, **kwargs)
|
|
91
|
+
app.app.logger.info("audit category=%s", options.get("audit_category"))
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
qivo.register_view_extension(AuditExtension())
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.route("/records", methods=["GET"])
|
|
101
|
+
@qivo.view(audit_category="records")
|
|
102
|
+
def records():
|
|
103
|
+
return []
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
An extension receives the Qivo instance, the next view callable, and an immutable
|
|
107
|
+
mapping of Qivo route options. Register extensions before declaring routes so they
|
|
108
|
+
wrap those routes. Place `@qivo.view(...)` directly below either `@app.route(...)`
|
|
109
|
+
or `@blueprint.route(...)`; Flask handles registration and lifecycle as usual.
|
|
110
|
+
|
|
111
|
+
## SQLAlchemy
|
|
112
|
+
|
|
113
|
+
Configure the database declaratively in Flask, then attach the SQL extension.
|
|
114
|
+
`SQLEngine` reads the URL, engine options, and session options from `app.config`
|
|
115
|
+
and configures the model base automatically. Each terminal query opens and closes
|
|
116
|
+
its own session:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from flask import Flask
|
|
120
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
121
|
+
|
|
122
|
+
from qivo import Qivo
|
|
123
|
+
from qivo.db.sql import Model
|
|
124
|
+
from qivo.db.sql.extensions import SQLEngine
|
|
125
|
+
|
|
126
|
+
app = Flask(__name__)
|
|
127
|
+
app.config.from_mapping(
|
|
128
|
+
SQLALCHEMY_DATABASE_URI="sqlite:///app.db",
|
|
129
|
+
SQLALCHEMY_ENGINE_OPTIONS={},
|
|
130
|
+
SQLALCHEMY_SESSION_OPTIONS={},
|
|
131
|
+
)
|
|
132
|
+
qivo = Qivo(app)
|
|
133
|
+
db = SQLEngine(app)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class User(Model):
|
|
137
|
+
__tablename__ = "users"
|
|
138
|
+
|
|
139
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
140
|
+
name: Mapped[str]
|
|
141
|
+
is_active: Mapped[bool]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
user = User(name="admin", is_active=True)
|
|
145
|
+
user.q.save()
|
|
146
|
+
|
|
147
|
+
user = User.q.get(user.id)
|
|
148
|
+
user.name = "root"
|
|
149
|
+
user.q.save()
|
|
150
|
+
|
|
151
|
+
user.q.delete() # Returns False if the row no longer exists.
|
|
152
|
+
|
|
153
|
+
admin = User.q.filter(name="admin").first()
|
|
154
|
+
active_admins = User.q.where(User.name == "admin").where(
|
|
155
|
+
User.is_active == True
|
|
156
|
+
).all()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use eager loading for relationships that must be accessed after a query returns,
|
|
160
|
+
because its session is closed when the operation completes. `SQLEngine` does not
|
|
161
|
+
create tables automatically; use the migration commands for schema changes.
|
|
162
|
+
|
|
163
|
+
## Migrations
|
|
164
|
+
|
|
165
|
+
`AlembicMigrations` uses `Model.metadata` and creates a standard `migrations/`
|
|
166
|
+
directory on the first revision. Import all model modules before autogenerating
|
|
167
|
+
so Alembic can see their tables:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from qivo.db.sql import AlembicMigrations
|
|
171
|
+
|
|
172
|
+
migrations = AlembicMigrations(engine)
|
|
173
|
+
migrations.revision("initial schema")
|
|
174
|
+
migrations.upgrade()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The directory and autogeneration options can be customized:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from qivo.db.sql import AlembicMigrations, MigrationConfig
|
|
181
|
+
|
|
182
|
+
migrations = AlembicMigrations(
|
|
183
|
+
engine,
|
|
184
|
+
config=MigrationConfig(directory="db/migrations", compare_type=True),
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use `migrations.downgrade()` to revert one revision or pass a target such as
|
|
189
|
+
`"base"`; use `migrations.stamp()` to mark a database without applying scripts.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qivo"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A small Flask layer for dependency injection, authorization, and response serialization."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"alembic>=1.20.0",
|
|
9
|
+
"flask>=3.1.3",
|
|
10
|
+
"sqlalchemy>=2.1.1",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
qivo = "qivo.cli:cli"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qivo"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A small Flask layer for dependency injection, authorization, and response serialization."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"alembic>=1.20.0",
|
|
9
|
+
"flask>=3.1.3",
|
|
10
|
+
"sqlalchemy>=2.1.1",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
qivo = "qivo.cli:cli"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from flask import Flask
|
|
6
|
+
|
|
7
|
+
from qivo.dependencies import DIContainer, DependencyInjectionExtension
|
|
8
|
+
from qivo.extensions import ViewExtension
|
|
9
|
+
from qivo.guards import Authenticator, AuthorizationExtension, Policy
|
|
10
|
+
from qivo.serialization import SerializationExtension, Serializer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Qivo:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
app: Flask | None = None,
|
|
17
|
+
serializer: Serializer | None = None,
|
|
18
|
+
container: DIContainer | None = None,
|
|
19
|
+
guards: Mapping[str, Authenticator] | None = None,
|
|
20
|
+
view_extensions: list[ViewExtension] | None = None,
|
|
21
|
+
):
|
|
22
|
+
self.app: Flask | None = None
|
|
23
|
+
self.serializer = serializer or Serializer()
|
|
24
|
+
self.container = container or DIContainer()
|
|
25
|
+
self.guards = dict(guards or {})
|
|
26
|
+
self.default_guard = "web"
|
|
27
|
+
self.view_extensions: list[ViewExtension] = [
|
|
28
|
+
AuthorizationExtension(),
|
|
29
|
+
DependencyInjectionExtension(),
|
|
30
|
+
SerializationExtension(),
|
|
31
|
+
]
|
|
32
|
+
for extension in view_extensions or ():
|
|
33
|
+
self.register_view_extension(extension)
|
|
34
|
+
|
|
35
|
+
if app is not None:
|
|
36
|
+
self.init_app(app)
|
|
37
|
+
|
|
38
|
+
def init_app(self, app: Flask) -> None:
|
|
39
|
+
"""Attach this Qivo extension to one Flask application."""
|
|
40
|
+
if self.app is not None and self.app is not app:
|
|
41
|
+
raise RuntimeError("A Qivo instance can be attached to only one Flask app")
|
|
42
|
+
|
|
43
|
+
existing = app.extensions.get("qivo")
|
|
44
|
+
if existing is not None and existing is not self:
|
|
45
|
+
raise RuntimeError("A Qivo extension is already registered on this app")
|
|
46
|
+
|
|
47
|
+
self.app = app
|
|
48
|
+
app.extensions["qivo"] = self
|
|
49
|
+
|
|
50
|
+
def register_view_extension(self, extension: ViewExtension) -> None:
|
|
51
|
+
"""Add a view wrapper; extensions run in registration order outside-in."""
|
|
52
|
+
self.view_extensions.append(extension)
|
|
53
|
+
|
|
54
|
+
def view(self, **options: Any):
|
|
55
|
+
"""Wrap a view with Qivo features without registering a Flask route."""
|
|
56
|
+
immutable_options = MappingProxyType(dict(options))
|
|
57
|
+
|
|
58
|
+
def decorator(view_func: Callable):
|
|
59
|
+
for extension in reversed(self.view_extensions):
|
|
60
|
+
view_func = extension.wrap_view(
|
|
61
|
+
self,
|
|
62
|
+
view_func,
|
|
63
|
+
options=immutable_options,
|
|
64
|
+
)
|
|
65
|
+
return view_func
|
|
66
|
+
|
|
67
|
+
return decorator
|
|
68
|
+
|
|
69
|
+
def guard(self, name: str | None = None):
|
|
70
|
+
guard_name = name or self.default_guard
|
|
71
|
+
return self.guards[guard_name]
|
|
72
|
+
|
|
73
|
+
def _serialize_view_response(self, rv):
|
|
74
|
+
target = rv
|
|
75
|
+
if isinstance(rv, tuple):
|
|
76
|
+
target = rv[0]
|
|
77
|
+
|
|
78
|
+
serialized = self.serializer.serialize(target)
|
|
79
|
+
|
|
80
|
+
if isinstance(rv, tuple) and len(rv) > 1:
|
|
81
|
+
return (serialized,) + rv[1:]
|
|
82
|
+
|
|
83
|
+
return serialized
|