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.
Files changed (72) hide show
  1. {pyferro-2.0.0 → pyferro-2.1.0}/PKG-INFO +19 -89
  2. {pyferro-2.0.0 → pyferro-2.1.0}/README.md +14 -87
  3. {pyferro-2.0.0 → pyferro-2.1.0}/pyproject.toml +18 -5
  4. {pyferro-2.0.0 → pyferro-2.1.0/src}/pyferro.egg-info/PKG-INFO +19 -89
  5. pyferro-2.1.0/src/pyferro.egg-info/SOURCES.txt +68 -0
  6. {pyferro-2.0.0 → pyferro-2.1.0/src}/pyferro.egg-info/requires.txt +4 -0
  7. pyferro-2.1.0/src/pyferro.egg-info/top_level.txt +1 -0
  8. pyferro-2.1.0/src/pyferro2/__init__.py +34 -0
  9. pyferro-2.1.0/src/pyferro2/__main__.py +15 -0
  10. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/seeder.py +2 -1
  11. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/constants.py +1 -0
  12. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/session.py +14 -3
  13. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/tabular.py +17 -2
  14. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/engine.py +45 -24
  15. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/spec.py +16 -2
  16. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/db/__init__.py +4 -0
  17. pyferro-2.1.0/src/pyferro2/db/pagination.py +333 -0
  18. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/__init__.py +11 -1
  19. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/storage.py +31 -4
  20. pyferro-2.1.0/src/pyferro2/frontend/dist/assets/index-qNsvt_pa.js +42 -0
  21. pyferro-2.1.0/src/pyferro2/frontend/dist/assets/index-v64oplBl.css +1 -0
  22. pyferro-2.1.0/src/pyferro2/frontend/dist/index.html +13 -0
  23. pyferro-2.1.0/src/pyferro2/help/__init__.py +7 -0
  24. pyferro-2.1.0/src/pyferro2/help/help_data.json +274 -0
  25. pyferro-2.1.0/src/pyferro2/help/rpc.py +93 -0
  26. pyferro-2.1.0/src/pyferro2/migrations/__init__.py +0 -0
  27. pyferro-2.1.0/src/pyferro2/migrations/migration_v2_0_0.py +27 -0
  28. pyferro-2.1.0/src/pyferro2/migrations/migration_v2_1_0.py +48 -0
  29. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/rpc/router.py +36 -19
  30. pyferro-2.1.0/src/pyferro2/seo/__init__.py +140 -0
  31. pyferro-2.1.0/src/pyferro2/seo/detector.py +141 -0
  32. pyferro-2.1.0/src/pyferro2/seo/indexnow.py +201 -0
  33. pyferro-2.1.0/src/pyferro2/seo/jinja_engine.py +473 -0
  34. pyferro-2.1.0/src/pyferro2/seo/models.py +162 -0
  35. pyferro-2.1.0/src/pyferro2/seo/page_cache.py +94 -0
  36. pyferro-2.1.0/src/pyferro2/seo/renderer.py +315 -0
  37. pyferro-2.1.0/src/pyferro2/seo/router.py +254 -0
  38. pyferro-2.1.0/src/pyferro2/seo/rpc.py +358 -0
  39. pyferro-2.1.0/src/pyferro2/seo/site_tree.py +1170 -0
  40. pyferro-2.1.0/src/pyferro2/seo/sitemap.py +253 -0
  41. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/app.py +169 -24
  42. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/tokens.py +19 -3
  43. pyferro-2.1.0/src/pyferro2/starters/__init__.py +18 -0
  44. pyferro-2.1.0/src/pyferro2/starters/catalog.py +40 -0
  45. pyferro-2.1.0/src/pyferro2/starters/manager.py +321 -0
  46. pyferro-2.1.0/src/pyferro2/starters/models.py +81 -0
  47. pyferro-2.1.0/src/pyferro2/starters/rpc.py +55 -0
  48. pyferro-2.1.0/src/pyferro2/starters/seeder.py +214 -0
  49. pyferro-2.1.0/src/pyferro2/starters/seo.py +180 -0
  50. pyferro-2.1.0/tests/test_pagination.py +179 -0
  51. pyferro-2.0.0/pyferro.egg-info/SOURCES.txt +0 -37
  52. pyferro-2.0.0/pyferro.egg-info/top_level.txt +0 -7
  53. {pyferro-2.0.0 → pyferro-2.1.0}/LICENSE +0 -0
  54. {pyferro-2.0.0 → pyferro-2.1.0}/setup.cfg +0 -0
  55. {pyferro-2.0.0 → pyferro-2.1.0/src}/pyferro.egg-info/dependency_links.txt +0 -0
  56. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/__init__.py +0 -0
  57. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/models.py +0 -0
  58. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/auth/passwords.py +0 -0
  59. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/__init__.py +0 -0
  60. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/core/session_data.py +0 -0
  61. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/__init__.py +0 -0
  62. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/crud/rpc.py +0 -0
  63. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/db/base.py +0 -0
  64. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/db/engine.py +0 -0
  65. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/models.py +0 -0
  66. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/files/rpc.py +0 -0
  67. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/rpc/__init__.py +0 -0
  68. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/rpc/errors.py +0 -0
  69. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/__init__.py +0 -0
  70. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/auth_rpc.py +0 -0
  71. {pyferro-2.0.0 → pyferro-2.1.0/src/pyferro2}/server/test_client.py +0 -0
  72. {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.0.0
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) и конфиги прокси (Nginx / Angie / Caddy)
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
- ### Полная боевая конфигурация Nginx / Angie для Granian (`WS /ws` + `2PC Files`)
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
- ### Конфигурация Caddy v2 (`Caddyfile`)
228
- Caddy понимает тот же самый заголовок `X-Accel-Redirect` через `handle_response`:
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) и конфиги прокси (Nginx / Angie / Caddy)
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
- ### Полная боевая конфигурация Nginx / Angie для Granian (`WS /ws` + `2PC Files`)
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
- ### Конфигурация Caddy v2 (`Caddyfile`)
206
- Caddy понимает тот же самый заголовок `X-Accel-Redirect` через `handle_response`:
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.0.0"
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 = ["auth*", "core*", "crud*", "db*", "files*", "rpc*", "server*", "pyferro2*"]
38
+ where = ["src"]
39
+ include = ["pyferro2*"]
34
40
 
35
41
  [tool.setuptools.package-data]
36
- "*" = ["frontend/dist/*", "frontend/dist/**/*"]
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.0.0
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) и конфиги прокси (Nginx / Angie / Caddy)
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
- ### Полная боевая конфигурация Nginx / Angie для Granian (`WS /ws` + `2PC Files`)
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
- ### Конфигурация Caddy v2 (`Caddyfile`)
228
- Caddy понимает тот же самый заголовок `X-Accel-Redirect` через `handle_response`:
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
@@ -6,3 +6,7 @@ jinja2>=3.1.0
6
6
  sqlalchemy[asyncio]>=2.0.0
7
7
  aiosqlite>=0.19.0
8
8
  greenlet>=3.0.0
9
+
10
+ [help-generator]
11
+ markdown-it-py>=3.0.0
12
+ pygments>=2.15.0
@@ -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
 
@@ -25,6 +25,7 @@ class admRole(StrEnum):
25
25
  adm_rpc = "adm_rpc"
26
26
  adm_help = "adm_help"
27
27
  adm_seo = "adm_seo"
28
+ adm_starters = "adm_starters"
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] = frozenset()) -> None:
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
- self.data = SessionData.guest(guest_permissions)
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
  """