pyferro 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyferro-2.0.0 → pyferro-2.1.0}/PKG-INFO +19 -89
- {pyferro-2.0.0 → pyferro-2.1.0}/README.md +14 -87
- {pyferro-2.0.0 → pyferro-2.1.0}/pyproject.toml +18 -5
- {pyferro-2.0.0 → pyferro-2.1.0/src}/pyferro.egg-info/PKG-INFO +19 -89
- pyferro-2.1.0/src/pyferro.egg-info/SOURCES.txt +68 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src}/pyferro.egg-info/requires.txt +4 -0
- pyferro-2.1.0/src/pyferro.egg-info/top_level.txt +1 -0
- pyferro-2.1.0/src/pyferro2/__init__.py +34 -0
- pyferro-2.1.0/src/pyferro2/__main__.py +15 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/seeder.py +2 -1
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/constants.py +1 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/session.py +14 -3
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/tabular.py +17 -2
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/engine.py +45 -24
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/spec.py +16 -2
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/db/__init__.py +4 -0
- pyferro-2.1.0/src/pyferro2/db/pagination.py +333 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/__init__.py +11 -1
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/storage.py +31 -4
- pyferro-2.1.0/src/pyferro2/frontend/dist/assets/index-qNsvt_pa.js +42 -0
- pyferro-2.1.0/src/pyferro2/frontend/dist/assets/index-v64oplBl.css +1 -0
- pyferro-2.1.0/src/pyferro2/frontend/dist/index.html +13 -0
- pyferro-2.1.0/src/pyferro2/help/__init__.py +7 -0
- pyferro-2.1.0/src/pyferro2/help/help_data.json +274 -0
- pyferro-2.1.0/src/pyferro2/help/rpc.py +93 -0
- pyferro-2.1.0/src/pyferro2/migrations/__init__.py +0 -0
- pyferro-2.1.0/src/pyferro2/migrations/migration_v2_0_0.py +27 -0
- pyferro-2.1.0/src/pyferro2/migrations/migration_v2_1_0.py +48 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/rpc/router.py +36 -19
- pyferro-2.1.0/src/pyferro2/seo/__init__.py +140 -0
- pyferro-2.1.0/src/pyferro2/seo/detector.py +141 -0
- pyferro-2.1.0/src/pyferro2/seo/indexnow.py +201 -0
- pyferro-2.1.0/src/pyferro2/seo/jinja_engine.py +473 -0
- pyferro-2.1.0/src/pyferro2/seo/models.py +162 -0
- pyferro-2.1.0/src/pyferro2/seo/page_cache.py +94 -0
- pyferro-2.1.0/src/pyferro2/seo/renderer.py +315 -0
- pyferro-2.1.0/src/pyferro2/seo/router.py +254 -0
- pyferro-2.1.0/src/pyferro2/seo/rpc.py +358 -0
- pyferro-2.1.0/src/pyferro2/seo/site_tree.py +1170 -0
- pyferro-2.1.0/src/pyferro2/seo/sitemap.py +253 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/app.py +169 -24
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/tokens.py +19 -3
- pyferro-2.1.0/src/pyferro2/starters/__init__.py +18 -0
- pyferro-2.1.0/src/pyferro2/starters/catalog.py +40 -0
- pyferro-2.1.0/src/pyferro2/starters/manager.py +321 -0
- pyferro-2.1.0/src/pyferro2/starters/models.py +81 -0
- pyferro-2.1.0/src/pyferro2/starters/rpc.py +55 -0
- pyferro-2.1.0/src/pyferro2/starters/seeder.py +214 -0
- pyferro-2.1.0/src/pyferro2/starters/seo.py +180 -0
- pyferro-2.1.0/tests/test_pagination.py +179 -0
- pyferro-2.0.0/pyferro.egg-info/SOURCES.txt +0 -37
- pyferro-2.0.0/pyferro.egg-info/top_level.txt +0 -7
- {pyferro-2.0.0 → pyferro-2.1.0}/LICENSE +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0}/setup.cfg +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src}/pyferro.egg-info/dependency_links.txt +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/__init__.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/models.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/passwords.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/__init__.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/session_data.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/__init__.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/rpc.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/db/base.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/db/engine.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/models.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/rpc.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/rpc/__init__.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/rpc/errors.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/__init__.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/auth_rpc.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/test_client.py +0 -0
- {pyferro-2.0.0 → pyferro-2.1.0}/tests/test_e2e_lifecycle.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyferro
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: High-performance reactive fullstack Python framework on Rust RSGI (Granian) with symmetric WSRPC (JSON-RPC 2.0) and Tabular Data Compression.
|
|
5
5
|
Author-email: Alex Titoff <a.tit.off@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/atitoff-dotcom/PyFerro
|
|
8
8
|
Project-URL: Repository, https://github.com/atitoff-dotcom/PyFerro.git
|
|
9
|
-
Keywords: pyferro,rsgi,granian,wsrpc,json-rpc,websocket,reactive
|
|
9
|
+
Keywords: pyferro,rsgi,granian,wsrpc,json-rpc,websocket,reactive,seo
|
|
10
10
|
Requires-Python: >=3.11
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
@@ -18,6 +18,9 @@ Requires-Dist: jinja2>=3.1.0
|
|
|
18
18
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
19
19
|
Requires-Dist: aiosqlite>=0.19.0
|
|
20
20
|
Requires-Dist: greenlet>=3.0.0
|
|
21
|
+
Provides-Extra: help-generator
|
|
22
|
+
Requires-Dist: markdown-it-py>=3.0.0; extra == "help-generator"
|
|
23
|
+
Requires-Dist: pygments>=2.15.0; extra == "help-generator"
|
|
21
24
|
Dynamic: license-file
|
|
22
25
|
|
|
23
26
|
# PyFerro 2.0 — Руководство разработчика и Архитектура
|
|
@@ -146,100 +149,17 @@ class feed:
|
|
|
146
149
|
|
|
147
150
|
---
|
|
148
151
|
|
|
149
|
-
## 4. Файловая подсистема (2PC) и
|
|
152
|
+
## 4. Файловая подсистема (2PC) и проксирование (Nginx / Angie / Caddy)
|
|
150
153
|
|
|
151
154
|
Загрузка файлов выполняется в 2 фазы (без буферизации в RAM):
|
|
152
155
|
1. Вызов RPC `files.init_upload` -> возвращает `folder_hash` и `upload_url`.
|
|
153
156
|
2. Потоковая отправка `POST /upload?folder=<folder_hash>&filename=<name>`.
|
|
154
157
|
3. Вызов RPC `files.commit(folder_hash=..., is_public=True)` -> атомарный перенос в постоянное хранилище (`os.replace`) и запись в `pf_file_metadata`.
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Для корректной работы **Granian** за Nginx/Angie важно:
|
|
159
|
-
1. Выделить `location /ws` с отключенной буферизацией (`proxy_buffering off`) и длинным `proxy_read_timeout`, чтобы Nginx не обрывал живые WebSocket-соединения и долгие мультиретурн-стримы (`yield`) каждые 60 секунд.
|
|
160
|
-
2. В `location /upload` отключить буферизацию запроса (`proxy_request_buffering off`), чтобы файлы стримились чанками напрямую в Granian за `O(1)` памяти.
|
|
161
|
-
3. В `location /internal_files/` включить `internal;` для прямой раздачи файлов с диска по заголовку `X-Accel-Redirect`.
|
|
162
|
-
|
|
163
|
-
```nginx
|
|
164
|
-
# В секции http { ... } определяем корректный заголовок Connection для WebSocket и Keep-Alive:
|
|
165
|
-
map $http_upgrade $connection_upgrade {
|
|
166
|
-
default upgrade;
|
|
167
|
-
'' close;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
upstream granian_backend {
|
|
171
|
-
server 127.0.0.1:8000;
|
|
172
|
-
keepalive 64;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
server {
|
|
176
|
-
listen 80;
|
|
177
|
-
server_name example.com;
|
|
178
|
-
|
|
179
|
-
# 1. WebSocket JSON-RPC 2.0 и мультиретурн (yield) для Granian
|
|
180
|
-
location /ws {
|
|
181
|
-
proxy_pass http://granian_backend/ws;
|
|
182
|
-
proxy_http_version 1.1;
|
|
183
|
-
|
|
184
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
185
|
-
proxy_set_header Connection $connection_upgrade;
|
|
186
|
-
proxy_set_header Host $host;
|
|
187
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
188
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
189
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
190
|
-
|
|
191
|
-
# Отключаем буферизацию для мгновенной доставки пакетов yield и session.notify
|
|
192
|
-
proxy_buffering off;
|
|
193
|
-
# Держим соединение для долгих подписок (24 часа; heartbeat ping/pong поддерживает активность)
|
|
194
|
-
proxy_read_timeout 86400s;
|
|
195
|
-
proxy_send_timeout 86400s;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# 2. Потоковая закачка файлов напрямую в Granian без двойной буферизации на диске Nginx
|
|
199
|
-
location /upload {
|
|
200
|
-
client_max_body_size 2G;
|
|
201
|
-
proxy_request_buffering off;
|
|
202
|
-
proxy_http_version 1.1;
|
|
203
|
-
proxy_set_header Host $host;
|
|
204
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
205
|
-
proxy_pass http://granian_backend/upload;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
# 3. Внутренняя раздача файлов с диска мимо Python (по заголовку X-Accel-Redirect от PyFerroApp)
|
|
209
|
-
location /internal_files/ {
|
|
210
|
-
internal;
|
|
211
|
-
alias /var/lib/pyferro/storage/;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
# 4. Остальные HTTP-маршруты (/rpc, /files/..., /pyferro_admin)
|
|
215
|
-
location / {
|
|
216
|
-
proxy_pass http://granian_backend;
|
|
217
|
-
proxy_http_version 1.1;
|
|
218
|
-
proxy_set_header Connection "";
|
|
219
|
-
proxy_set_header Host $host;
|
|
220
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
221
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
222
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
```
|
|
159
|
+
Раздача защищенных файлов осуществляется через заголовок `X-Accel-Redirect` без прогона байт через Python.
|
|
226
160
|
|
|
227
|
-
|
|
228
|
-
Caddy
|
|
229
|
-
|
|
230
|
-
```caddyfile
|
|
231
|
-
example.com {
|
|
232
|
-
reverse_proxy 127.0.0.1:8000 {
|
|
233
|
-
@accel header X-Accel-Redirect *
|
|
234
|
-
handle_response @accel {
|
|
235
|
-
root * /var/lib/pyferro/storage
|
|
236
|
-
rewrite * {rp.header.X-Accel-Redirect}
|
|
237
|
-
uri strip_prefix /internal_files
|
|
238
|
-
file_server
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
```
|
|
161
|
+
> [!TIP]
|
|
162
|
+
> **Боевые конфигурации Nginx, Angie и Caddy v2**, архитектура потокового отсечения неавторизованных загрузок (Fail-Fast) и раздачи статики подробно описаны в [NGINX.md](NGINX.md).
|
|
243
163
|
|
|
244
164
|
---
|
|
245
165
|
|
|
@@ -252,3 +172,13 @@ example.com {
|
|
|
252
172
|
| `POST /upload` | Потоковая закачка файлов во временную транзакцию (`O(1)` RAM) |
|
|
253
173
|
| `GET /files/<folder>/<name>` | Защищенная раздача файлов (поддержка `X-Accel-Redirect` и файловых JWT) |
|
|
254
174
|
| `GET /pyferro_admin` | Встроенная панель управления RBAC и CRUD (маршрут `/admin` свободен для вашего приложения) |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 6. Табличная пагинация и сжатие трафика ($tabular)
|
|
179
|
+
|
|
180
|
+
PyFerro 2.0 включает высокопроизводительную подсистему пагинации для SQLAlchemy и фронтенда:
|
|
181
|
+
|
|
182
|
+
- **Бэкенд:** `paginate_tabular(db, Model, **params)` — выполняет поиск, фильтры, сортировку, умный `count(*)` и пакует ответ в сжатый протокол `$tabular` за **1 строчку кода**.
|
|
183
|
+
- **Фронтенд:** `createPaginator("Model.read")` + компонент `<Pagination />` — полностью берут на себя состояние, номера страниц `[1, '...', 4, 5, 6, '...', 25]`, стрелки «Назад/Вперед» и живой поиск.
|
|
184
|
+
- **Подробное руководство:** см. [PAGINATION.md](PAGINATION.md).
|
|
@@ -124,100 +124,17 @@ class feed:
|
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
127
|
-
## 4. Файловая подсистема (2PC) и
|
|
127
|
+
## 4. Файловая подсистема (2PC) и проксирование (Nginx / Angie / Caddy)
|
|
128
128
|
|
|
129
129
|
Загрузка файлов выполняется в 2 фазы (без буферизации в RAM):
|
|
130
130
|
1. Вызов RPC `files.init_upload` -> возвращает `folder_hash` и `upload_url`.
|
|
131
131
|
2. Потоковая отправка `POST /upload?folder=<folder_hash>&filename=<name>`.
|
|
132
132
|
3. Вызов RPC `files.commit(folder_hash=..., is_public=True)` -> атомарный перенос в постоянное хранилище (`os.replace`) и запись в `pf_file_metadata`.
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Для корректной работы **Granian** за Nginx/Angie важно:
|
|
137
|
-
1. Выделить `location /ws` с отключенной буферизацией (`proxy_buffering off`) и длинным `proxy_read_timeout`, чтобы Nginx не обрывал живые WebSocket-соединения и долгие мультиретурн-стримы (`yield`) каждые 60 секунд.
|
|
138
|
-
2. В `location /upload` отключить буферизацию запроса (`proxy_request_buffering off`), чтобы файлы стримились чанками напрямую в Granian за `O(1)` памяти.
|
|
139
|
-
3. В `location /internal_files/` включить `internal;` для прямой раздачи файлов с диска по заголовку `X-Accel-Redirect`.
|
|
140
|
-
|
|
141
|
-
```nginx
|
|
142
|
-
# В секции http { ... } определяем корректный заголовок Connection для WebSocket и Keep-Alive:
|
|
143
|
-
map $http_upgrade $connection_upgrade {
|
|
144
|
-
default upgrade;
|
|
145
|
-
'' close;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
upstream granian_backend {
|
|
149
|
-
server 127.0.0.1:8000;
|
|
150
|
-
keepalive 64;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
server {
|
|
154
|
-
listen 80;
|
|
155
|
-
server_name example.com;
|
|
156
|
-
|
|
157
|
-
# 1. WebSocket JSON-RPC 2.0 и мультиретурн (yield) для Granian
|
|
158
|
-
location /ws {
|
|
159
|
-
proxy_pass http://granian_backend/ws;
|
|
160
|
-
proxy_http_version 1.1;
|
|
161
|
-
|
|
162
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
163
|
-
proxy_set_header Connection $connection_upgrade;
|
|
164
|
-
proxy_set_header Host $host;
|
|
165
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
166
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
167
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
168
|
-
|
|
169
|
-
# Отключаем буферизацию для мгновенной доставки пакетов yield и session.notify
|
|
170
|
-
proxy_buffering off;
|
|
171
|
-
# Держим соединение для долгих подписок (24 часа; heartbeat ping/pong поддерживает активность)
|
|
172
|
-
proxy_read_timeout 86400s;
|
|
173
|
-
proxy_send_timeout 86400s;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
# 2. Потоковая закачка файлов напрямую в Granian без двойной буферизации на диске Nginx
|
|
177
|
-
location /upload {
|
|
178
|
-
client_max_body_size 2G;
|
|
179
|
-
proxy_request_buffering off;
|
|
180
|
-
proxy_http_version 1.1;
|
|
181
|
-
proxy_set_header Host $host;
|
|
182
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
183
|
-
proxy_pass http://granian_backend/upload;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
# 3. Внутренняя раздача файлов с диска мимо Python (по заголовку X-Accel-Redirect от PyFerroApp)
|
|
187
|
-
location /internal_files/ {
|
|
188
|
-
internal;
|
|
189
|
-
alias /var/lib/pyferro/storage/;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
# 4. Остальные HTTP-маршруты (/rpc, /files/..., /pyferro_admin)
|
|
193
|
-
location / {
|
|
194
|
-
proxy_pass http://granian_backend;
|
|
195
|
-
proxy_http_version 1.1;
|
|
196
|
-
proxy_set_header Connection "";
|
|
197
|
-
proxy_set_header Host $host;
|
|
198
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
199
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
200
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
```
|
|
134
|
+
Раздача защищенных файлов осуществляется через заголовок `X-Accel-Redirect` без прогона байт через Python.
|
|
204
135
|
|
|
205
|
-
|
|
206
|
-
Caddy
|
|
207
|
-
|
|
208
|
-
```caddyfile
|
|
209
|
-
example.com {
|
|
210
|
-
reverse_proxy 127.0.0.1:8000 {
|
|
211
|
-
@accel header X-Accel-Redirect *
|
|
212
|
-
handle_response @accel {
|
|
213
|
-
root * /var/lib/pyferro/storage
|
|
214
|
-
rewrite * {rp.header.X-Accel-Redirect}
|
|
215
|
-
uri strip_prefix /internal_files
|
|
216
|
-
file_server
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
```
|
|
136
|
+
> [!TIP]
|
|
137
|
+
> **Боевые конфигурации Nginx, Angie и Caddy v2**, архитектура потокового отсечения неавторизованных загрузок (Fail-Fast) и раздачи статики подробно описаны в [NGINX.md](NGINX.md).
|
|
221
138
|
|
|
222
139
|
---
|
|
223
140
|
|
|
@@ -230,3 +147,13 @@ example.com {
|
|
|
230
147
|
| `POST /upload` | Потоковая закачка файлов во временную транзакцию (`O(1)` RAM) |
|
|
231
148
|
| `GET /files/<folder>/<name>` | Защищенная раздача файлов (поддержка `X-Accel-Redirect` и файловых JWT) |
|
|
232
149
|
| `GET /pyferro_admin` | Встроенная панель управления RBAC и CRUD (маршрут `/admin` свободен для вашего приложения) |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 6. Табличная пагинация и сжатие трафика ($tabular)
|
|
154
|
+
|
|
155
|
+
PyFerro 2.0 включает высокопроизводительную подсистему пагинации для SQLAlchemy и фронтенда:
|
|
156
|
+
|
|
157
|
+
- **Бэкенд:** `paginate_tabular(db, Model, **params)` — выполняет поиск, фильтры, сортировку, умный `count(*)` и пакует ответ в сжатый протокол `$tabular` за **1 строчку кода**.
|
|
158
|
+
- **Фронтенд:** `createPaginator("Model.read")` + компонент `<Pagination />` — полностью берут на себя состояние, номера страниц `[1, '...', 4, 5, 6, '...', 25]`, стрелки «Назад/Вперед» и живой поиск.
|
|
159
|
+
- **Подробное руководство:** см. [PAGINATION.md](PAGINATION.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyferro"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "High-performance reactive fullstack Python framework on Rust RSGI (Granian) with symmetric WSRPC (JSON-RPC 2.0) and Tabular Data Compression."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ license = { text = "MIT" }
|
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Alex Titoff", email = "a.tit.off@gmail.com" }
|
|
14
14
|
]
|
|
15
|
-
keywords = ["pyferro", "rsgi", "granian", "wsrpc", "json-rpc", "websocket", "reactive"]
|
|
15
|
+
keywords = ["pyferro", "rsgi", "granian", "wsrpc", "json-rpc", "websocket", "reactive", "seo"]
|
|
16
16
|
dependencies = [
|
|
17
17
|
"granian>=1.6.0",
|
|
18
18
|
"orjson>=3.9.0",
|
|
@@ -24,13 +24,26 @@ dependencies = [
|
|
|
24
24
|
"greenlet>=3.0.0",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
help-generator = [
|
|
29
|
+
"markdown-it-py>=3.0.0",
|
|
30
|
+
"pygments>=2.15.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
27
33
|
[project.urls]
|
|
28
34
|
Homepage = "https://github.com/atitoff-dotcom/PyFerro"
|
|
29
35
|
Repository = "https://github.com/atitoff-dotcom/PyFerro.git"
|
|
30
36
|
|
|
31
37
|
[tool.setuptools.packages.find]
|
|
32
|
-
where = ["
|
|
33
|
-
include = ["
|
|
38
|
+
where = ["src"]
|
|
39
|
+
include = ["pyferro2*"]
|
|
34
40
|
|
|
35
41
|
[tool.setuptools.package-data]
|
|
36
|
-
"
|
|
42
|
+
"pyferro2" = [
|
|
43
|
+
"help/help_data.json",
|
|
44
|
+
"frontend/dist/*",
|
|
45
|
+
"frontend/dist/**/*",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
pythonpath = [".", "src"]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyferro
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: High-performance reactive fullstack Python framework on Rust RSGI (Granian) with symmetric WSRPC (JSON-RPC 2.0) and Tabular Data Compression.
|
|
5
5
|
Author-email: Alex Titoff <a.tit.off@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/atitoff-dotcom/PyFerro
|
|
8
8
|
Project-URL: Repository, https://github.com/atitoff-dotcom/PyFerro.git
|
|
9
|
-
Keywords: pyferro,rsgi,granian,wsrpc,json-rpc,websocket,reactive
|
|
9
|
+
Keywords: pyferro,rsgi,granian,wsrpc,json-rpc,websocket,reactive,seo
|
|
10
10
|
Requires-Python: >=3.11
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
@@ -18,6 +18,9 @@ Requires-Dist: jinja2>=3.1.0
|
|
|
18
18
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
19
19
|
Requires-Dist: aiosqlite>=0.19.0
|
|
20
20
|
Requires-Dist: greenlet>=3.0.0
|
|
21
|
+
Provides-Extra: help-generator
|
|
22
|
+
Requires-Dist: markdown-it-py>=3.0.0; extra == "help-generator"
|
|
23
|
+
Requires-Dist: pygments>=2.15.0; extra == "help-generator"
|
|
21
24
|
Dynamic: license-file
|
|
22
25
|
|
|
23
26
|
# PyFerro 2.0 — Руководство разработчика и Архитектура
|
|
@@ -146,100 +149,17 @@ class feed:
|
|
|
146
149
|
|
|
147
150
|
---
|
|
148
151
|
|
|
149
|
-
## 4. Файловая подсистема (2PC) и
|
|
152
|
+
## 4. Файловая подсистема (2PC) и проксирование (Nginx / Angie / Caddy)
|
|
150
153
|
|
|
151
154
|
Загрузка файлов выполняется в 2 фазы (без буферизации в RAM):
|
|
152
155
|
1. Вызов RPC `files.init_upload` -> возвращает `folder_hash` и `upload_url`.
|
|
153
156
|
2. Потоковая отправка `POST /upload?folder=<folder_hash>&filename=<name>`.
|
|
154
157
|
3. Вызов RPC `files.commit(folder_hash=..., is_public=True)` -> атомарный перенос в постоянное хранилище (`os.replace`) и запись в `pf_file_metadata`.
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Для корректной работы **Granian** за Nginx/Angie важно:
|
|
159
|
-
1. Выделить `location /ws` с отключенной буферизацией (`proxy_buffering off`) и длинным `proxy_read_timeout`, чтобы Nginx не обрывал живые WebSocket-соединения и долгие мультиретурн-стримы (`yield`) каждые 60 секунд.
|
|
160
|
-
2. В `location /upload` отключить буферизацию запроса (`proxy_request_buffering off`), чтобы файлы стримились чанками напрямую в Granian за `O(1)` памяти.
|
|
161
|
-
3. В `location /internal_files/` включить `internal;` для прямой раздачи файлов с диска по заголовку `X-Accel-Redirect`.
|
|
162
|
-
|
|
163
|
-
```nginx
|
|
164
|
-
# В секции http { ... } определяем корректный заголовок Connection для WebSocket и Keep-Alive:
|
|
165
|
-
map $http_upgrade $connection_upgrade {
|
|
166
|
-
default upgrade;
|
|
167
|
-
'' close;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
upstream granian_backend {
|
|
171
|
-
server 127.0.0.1:8000;
|
|
172
|
-
keepalive 64;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
server {
|
|
176
|
-
listen 80;
|
|
177
|
-
server_name example.com;
|
|
178
|
-
|
|
179
|
-
# 1. WebSocket JSON-RPC 2.0 и мультиретурн (yield) для Granian
|
|
180
|
-
location /ws {
|
|
181
|
-
proxy_pass http://granian_backend/ws;
|
|
182
|
-
proxy_http_version 1.1;
|
|
183
|
-
|
|
184
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
185
|
-
proxy_set_header Connection $connection_upgrade;
|
|
186
|
-
proxy_set_header Host $host;
|
|
187
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
188
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
189
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
190
|
-
|
|
191
|
-
# Отключаем буферизацию для мгновенной доставки пакетов yield и session.notify
|
|
192
|
-
proxy_buffering off;
|
|
193
|
-
# Держим соединение для долгих подписок (24 часа; heartbeat ping/pong поддерживает активность)
|
|
194
|
-
proxy_read_timeout 86400s;
|
|
195
|
-
proxy_send_timeout 86400s;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# 2. Потоковая закачка файлов напрямую в Granian без двойной буферизации на диске Nginx
|
|
199
|
-
location /upload {
|
|
200
|
-
client_max_body_size 2G;
|
|
201
|
-
proxy_request_buffering off;
|
|
202
|
-
proxy_http_version 1.1;
|
|
203
|
-
proxy_set_header Host $host;
|
|
204
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
205
|
-
proxy_pass http://granian_backend/upload;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
# 3. Внутренняя раздача файлов с диска мимо Python (по заголовку X-Accel-Redirect от PyFerroApp)
|
|
209
|
-
location /internal_files/ {
|
|
210
|
-
internal;
|
|
211
|
-
alias /var/lib/pyferro/storage/;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
# 4. Остальные HTTP-маршруты (/rpc, /files/..., /pyferro_admin)
|
|
215
|
-
location / {
|
|
216
|
-
proxy_pass http://granian_backend;
|
|
217
|
-
proxy_http_version 1.1;
|
|
218
|
-
proxy_set_header Connection "";
|
|
219
|
-
proxy_set_header Host $host;
|
|
220
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
221
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
222
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
```
|
|
159
|
+
Раздача защищенных файлов осуществляется через заголовок `X-Accel-Redirect` без прогона байт через Python.
|
|
226
160
|
|
|
227
|
-
|
|
228
|
-
Caddy
|
|
229
|
-
|
|
230
|
-
```caddyfile
|
|
231
|
-
example.com {
|
|
232
|
-
reverse_proxy 127.0.0.1:8000 {
|
|
233
|
-
@accel header X-Accel-Redirect *
|
|
234
|
-
handle_response @accel {
|
|
235
|
-
root * /var/lib/pyferro/storage
|
|
236
|
-
rewrite * {rp.header.X-Accel-Redirect}
|
|
237
|
-
uri strip_prefix /internal_files
|
|
238
|
-
file_server
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
```
|
|
161
|
+
> [!TIP]
|
|
162
|
+
> **Боевые конфигурации Nginx, Angie и Caddy v2**, архитектура потокового отсечения неавторизованных загрузок (Fail-Fast) и раздачи статики подробно описаны в [NGINX.md](NGINX.md).
|
|
243
163
|
|
|
244
164
|
---
|
|
245
165
|
|
|
@@ -252,3 +172,13 @@ example.com {
|
|
|
252
172
|
| `POST /upload` | Потоковая закачка файлов во временную транзакцию (`O(1)` RAM) |
|
|
253
173
|
| `GET /files/<folder>/<name>` | Защищенная раздача файлов (поддержка `X-Accel-Redirect` и файловых JWT) |
|
|
254
174
|
| `GET /pyferro_admin` | Встроенная панель управления RBAC и CRUD (маршрут `/admin` свободен для вашего приложения) |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 6. Табличная пагинация и сжатие трафика ($tabular)
|
|
179
|
+
|
|
180
|
+
PyFerro 2.0 включает высокопроизводительную подсистему пагинации для SQLAlchemy и фронтенда:
|
|
181
|
+
|
|
182
|
+
- **Бэкенд:** `paginate_tabular(db, Model, **params)` — выполняет поиск, фильтры, сортировку, умный `count(*)` и пакует ответ в сжатый протокол `$tabular` за **1 строчку кода**.
|
|
183
|
+
- **Фронтенд:** `createPaginator("Model.read")` + компонент `<Pagination />` — полностью берут на себя состояние, номера страниц `[1, '...', 4, 5, 6, '...', 25]`, стрелки «Назад/Вперед» и живой поиск.
|
|
184
|
+
- **Подробное руководство:** см. [PAGINATION.md](PAGINATION.md).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pyferro.egg-info/PKG-INFO
|
|
5
|
+
src/pyferro.egg-info/SOURCES.txt
|
|
6
|
+
src/pyferro.egg-info/dependency_links.txt
|
|
7
|
+
src/pyferro.egg-info/requires.txt
|
|
8
|
+
src/pyferro.egg-info/top_level.txt
|
|
9
|
+
src/pyferro2/__init__.py
|
|
10
|
+
src/pyferro2/__main__.py
|
|
11
|
+
src/pyferro2/auth/__init__.py
|
|
12
|
+
src/pyferro2/auth/models.py
|
|
13
|
+
src/pyferro2/auth/passwords.py
|
|
14
|
+
src/pyferro2/auth/seeder.py
|
|
15
|
+
src/pyferro2/core/__init__.py
|
|
16
|
+
src/pyferro2/core/constants.py
|
|
17
|
+
src/pyferro2/core/session.py
|
|
18
|
+
src/pyferro2/core/session_data.py
|
|
19
|
+
src/pyferro2/core/tabular.py
|
|
20
|
+
src/pyferro2/crud/__init__.py
|
|
21
|
+
src/pyferro2/crud/engine.py
|
|
22
|
+
src/pyferro2/crud/rpc.py
|
|
23
|
+
src/pyferro2/crud/spec.py
|
|
24
|
+
src/pyferro2/db/__init__.py
|
|
25
|
+
src/pyferro2/db/base.py
|
|
26
|
+
src/pyferro2/db/engine.py
|
|
27
|
+
src/pyferro2/db/pagination.py
|
|
28
|
+
src/pyferro2/files/__init__.py
|
|
29
|
+
src/pyferro2/files/models.py
|
|
30
|
+
src/pyferro2/files/rpc.py
|
|
31
|
+
src/pyferro2/files/storage.py
|
|
32
|
+
src/pyferro2/frontend/dist/index.html
|
|
33
|
+
src/pyferro2/frontend/dist/assets/index-qNsvt_pa.js
|
|
34
|
+
src/pyferro2/frontend/dist/assets/index-v64oplBl.css
|
|
35
|
+
src/pyferro2/help/__init__.py
|
|
36
|
+
src/pyferro2/help/help_data.json
|
|
37
|
+
src/pyferro2/help/rpc.py
|
|
38
|
+
src/pyferro2/migrations/__init__.py
|
|
39
|
+
src/pyferro2/migrations/migration_v2_0_0.py
|
|
40
|
+
src/pyferro2/migrations/migration_v2_1_0.py
|
|
41
|
+
src/pyferro2/rpc/__init__.py
|
|
42
|
+
src/pyferro2/rpc/errors.py
|
|
43
|
+
src/pyferro2/rpc/router.py
|
|
44
|
+
src/pyferro2/seo/__init__.py
|
|
45
|
+
src/pyferro2/seo/detector.py
|
|
46
|
+
src/pyferro2/seo/indexnow.py
|
|
47
|
+
src/pyferro2/seo/jinja_engine.py
|
|
48
|
+
src/pyferro2/seo/models.py
|
|
49
|
+
src/pyferro2/seo/page_cache.py
|
|
50
|
+
src/pyferro2/seo/renderer.py
|
|
51
|
+
src/pyferro2/seo/router.py
|
|
52
|
+
src/pyferro2/seo/rpc.py
|
|
53
|
+
src/pyferro2/seo/site_tree.py
|
|
54
|
+
src/pyferro2/seo/sitemap.py
|
|
55
|
+
src/pyferro2/server/__init__.py
|
|
56
|
+
src/pyferro2/server/app.py
|
|
57
|
+
src/pyferro2/server/auth_rpc.py
|
|
58
|
+
src/pyferro2/server/test_client.py
|
|
59
|
+
src/pyferro2/server/tokens.py
|
|
60
|
+
src/pyferro2/starters/__init__.py
|
|
61
|
+
src/pyferro2/starters/catalog.py
|
|
62
|
+
src/pyferro2/starters/manager.py
|
|
63
|
+
src/pyferro2/starters/models.py
|
|
64
|
+
src/pyferro2/starters/rpc.py
|
|
65
|
+
src/pyferro2/starters/seeder.py
|
|
66
|
+
src/pyferro2/starters/seo.py
|
|
67
|
+
tests/test_e2e_lifecycle.py
|
|
68
|
+
tests/test_pagination.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyferro2
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyFerro 2.0 — высокопроизводительный веб-фреймворк с минимальным потреблением CPU и zero-boilerplate.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pyferro2.db.pagination import decode_cursor, encode_cursor, paginate_tabular
|
|
6
|
+
from pyferro2.seo import (
|
|
7
|
+
HtmlBuilder,
|
|
8
|
+
SeoConfig,
|
|
9
|
+
SeoPageData,
|
|
10
|
+
bot_page,
|
|
11
|
+
configure_seo,
|
|
12
|
+
get_seo_config,
|
|
13
|
+
is_bot,
|
|
14
|
+
notify_indexnow,
|
|
15
|
+
register_sitemap_provider,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "2.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"paginate_tabular",
|
|
23
|
+
"encode_cursor",
|
|
24
|
+
"decode_cursor",
|
|
25
|
+
"SeoConfig",
|
|
26
|
+
"SeoPageData",
|
|
27
|
+
"HtmlBuilder",
|
|
28
|
+
"configure_seo",
|
|
29
|
+
"get_seo_config",
|
|
30
|
+
"is_bot",
|
|
31
|
+
"bot_page",
|
|
32
|
+
"register_sitemap_provider",
|
|
33
|
+
"notify_indexnow",
|
|
34
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI-точка входа для выполнения служебных команд PyFerro 2.0 (например, python -m pyferro2 --add-admin).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pyferro2.server.app import PyFerroApp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
app = PyFerroApp()
|
|
10
|
+
if not app.handle_cli():
|
|
11
|
+
print("Использование: python -m pyferro2 --add-admin [--username admin] [--password admin]")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|
|
@@ -23,8 +23,9 @@ SYSTEM_ROLE_DESCRIPTIONS: dict[str, str] = {
|
|
|
23
23
|
admRole.adm_roles.value: "Доступ к разделу управления ролями и матрицей прав",
|
|
24
24
|
admRole.adm_settings.value: "Доступ к системным настройкам",
|
|
25
25
|
admRole.adm_rpc.value: "Доступ к инспектору и правам JSON-RPC методов",
|
|
26
|
-
admRole.adm_help.value: "
|
|
26
|
+
admRole.adm_help.value: "Чтение справки PyFerro2",
|
|
27
27
|
admRole.adm_seo.value: "Доступ к управлению SEO-правилами и страницами",
|
|
28
|
+
admRole.adm_starters.value: "Доступ к разделу шаблонов и примеров",
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
|
|
@@ -30,6 +30,7 @@ class Session:
|
|
|
30
30
|
__slots__ = (
|
|
31
31
|
"session_id",
|
|
32
32
|
"data",
|
|
33
|
+
"_guest_permissions",
|
|
33
34
|
"_notify",
|
|
34
35
|
"_close",
|
|
35
36
|
"_active_rpcs",
|
|
@@ -44,6 +45,7 @@ class Session:
|
|
|
44
45
|
data: SessionData = GUEST_SESSION_DATA,
|
|
45
46
|
notify: NotifyCallback | None = None,
|
|
46
47
|
close: CloseCallback | None = None,
|
|
48
|
+
guest_permissions: frozenset[str] | None = None,
|
|
47
49
|
) -> None:
|
|
48
50
|
if type(session_id) is not str or not session_id:
|
|
49
51
|
raise ValueError("session_id должен быть непустой строкой")
|
|
@@ -56,6 +58,12 @@ class Session:
|
|
|
56
58
|
|
|
57
59
|
self.session_id: str = session_id
|
|
58
60
|
self.data: SessionData = data
|
|
61
|
+
if guest_permissions is not None:
|
|
62
|
+
self._guest_permissions: frozenset[str] = frozenset(guest_permissions)
|
|
63
|
+
elif data.is_guest:
|
|
64
|
+
self._guest_permissions = data.permissions
|
|
65
|
+
else:
|
|
66
|
+
self._guest_permissions = GUEST_SESSION_DATA.permissions
|
|
59
67
|
self._notify: NotifyCallback | None = notify
|
|
60
68
|
self._close: CloseCallback | None = close
|
|
61
69
|
self._active_rpcs: int = 0
|
|
@@ -76,11 +84,14 @@ class Session:
|
|
|
76
84
|
raise TypeError(f"new_data должен быть экземпляром SessionData, получено: {type(new_data).__name__}")
|
|
77
85
|
self.data = new_data
|
|
78
86
|
|
|
79
|
-
def reset_to_guest(self, guest_permissions: frozenset[str] =
|
|
87
|
+
def reset_to_guest(self, guest_permissions: frozenset[str] | None = None) -> None:
|
|
80
88
|
"""
|
|
81
|
-
Сбрасывает сессию обратно в гостевое состояние (при вызове `auth.logout`)
|
|
89
|
+
Сбрасывает сессию обратно в гостевое состояние (при вызове `auth.logout`),
|
|
90
|
+
сохраняя актуальные разрешения роли `guest`.
|
|
82
91
|
"""
|
|
83
|
-
|
|
92
|
+
if guest_permissions is not None:
|
|
93
|
+
self._guest_permissions = frozenset(guest_permissions)
|
|
94
|
+
self.data = SessionData.guest(self._guest_permissions)
|
|
84
95
|
|
|
85
96
|
async def notify(self, event: str, payload: Any = None) -> None:
|
|
86
97
|
"""
|