pyferro-demo 0.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.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyferro-demo
3
+ Version: 0.1.0
4
+ Summary: Showcase & educational demo application built on the PyFerro framework (SEO Forum, JSON-RPC, Granian RSGI).
5
+ Author-email: Alex Titoff <a.tit.off@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/atitoff-dotcom/PyFerro
8
+ Project-URL: Repository, https://github.com/atitoff-dotcom/PyFerro.git
9
+ Keywords: pyferro,demo,forum,showcase,rsgi,wsrpc
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pyferro>=2.1.0
13
+
14
+ # πŸš€ PyFerro Showcase & Demo Application
15
+
16
+ ΠžΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΠΎΠ΅ дСмонстрационноС ΠΈ ΠΎΠ±ΡƒΡ‡Π°ΡŽΡ‰Π΅Π΅ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π° Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠΌ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ΅ **PyFerro**.
17
+ Π‘Π»ΡƒΠΆΠΈΡ‚ ΠΎΠ±Ρ€Π°Π·Ρ†ΠΎΠΌ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹, ΠΆΠΈΠ²Ρ‹ΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ΠΎΠΌ использования ΠΈ E2E-тСстСром ядра.
18
+
19
+ ---
20
+
21
+ ## πŸ’‘ ΠžΡΠΎΠ±Π΅Π½Π½ΠΎΡΡ‚ΠΈ
22
+
23
+ - **Π˜Π·ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Π½Π½Π°Ρ Π±Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…:** ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ создаСт ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Π»ΠΎΠΊΠ°Π»ΡŒΠ½ΡƒΡŽ Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… `demo.db` (SQLite). Π’Π°ΡˆΠ° корпоративная Π‘Π” остаСтся Π² ΠΏΠΎΠ»Π½ΠΎΠΉ бСзопасности.
24
+ - **ΠŸΠΎΠ»Π½ΠΎΡ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ SEO-Ρ„ΠΎΡ€ΡƒΠΌ:**
25
+ - ДрСвовидная структура Ρ€Π°Π·Π΄Π΅Π»ΠΎΠ² ΠΈ Π²Π΅Ρ‚ΠΎΠΊ (`Topic`, `Message`).
26
+ - ΠœΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠ° Schema.org (`BreadcrumbList`, `DiscussionForumPosting` Π² JSON-LD).
27
+ - ДинамичСский Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³ страниц для поисковых Π±ΠΎΡ‚ΠΎΠ² (Jinja2).
28
+ - **Π‘ΠΈΠΌΠΌΠ΅Ρ‚Ρ€ΠΈΡ‡Π½Ρ‹ΠΉ JSON-RPC 2.0:** ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² с ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π΅ΠΌ Ρ€ΠΎΠ»Π΅ΠΉ (RBAC).
29
+ - **Π“ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ сид-Π΄Π°Π½Π½Ρ‹Π΅:** АвтоматичСскоС Π½Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ 10 Ρ‚Π΅ΠΌΠ°ΠΌΠΈ ΠΈ Π²Π΅Ρ‚ΠΊΠ°ΠΌΠΈ обсуТдСний ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΌ запускС.
30
+
31
+ ---
32
+
33
+ ## ⚑ Быстрый старт
34
+
35
+ ### 1. Установка ΠΈΠ· PyPI:
36
+ ```bash
37
+ pip install pyferro-demo
38
+ ```
39
+
40
+ ### 2. Запуск прилоТСния:
41
+ ```bash
42
+ pyferro-demo run
43
+ ```
44
+ ΠΈΠ»ΠΈ Π±Π΅Π· ΠΏΡ€Π΅Π΄Π²Π°Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΉ установки:
45
+ ```bash
46
+ pipx run pyferro-demo
47
+ ```
48
+
49
+ ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ запустится Π½Π° Rust-сСрвСрС Granian:
50
+ - **Админка PyFerro:** [http://localhost:8080/pyferro_admin/](http://localhost:8080/pyferro_admin/) (Π»ΠΎΠ³ΠΈΠ½: `admin`, ΠΏΠ°Ρ€ΠΎΠ»ΡŒ: `admin`)
51
+ - **JSON-RPC эндпоинт:** `ws://localhost:8080/wsrpc`
52
+ - **Sitemap XML:** [http://localhost:8080/sitemap.xml](http://localhost:8080/sitemap.xml)
53
+
54
+ ---
55
+
56
+ ## πŸ“‚ Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°
57
+
58
+ ```text
59
+ pyferro_demo/
60
+ β”œβ”€β”€ cli.py # Π’ΠΎΡ‡ΠΊΠ° Π²Ρ…ΠΎΠ΄Π° консольной ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ pyferro-demo
61
+ β”œβ”€β”€ app.py # Π€Π°Π±Ρ€ΠΈΠΊΠ° прилоТСния PyFerroApp ΠΈ рСгистрация ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ
62
+ β”œβ”€β”€ forum/
63
+ β”‚ β”œβ”€β”€ models.py # SQLAlchemy-ΠΌΠΎΠ΄Π΅Π»ΠΈ Ρ‚ΠΎΠΏΠΈΠΊΠΎΠ² ΠΈ сообщСний
64
+ β”‚ β”œβ”€β”€ rpc.py # JSON-RPC 2.0 ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ (forum.list_topics, forum.get_topic ΠΈ Ρ‚.Π΄.)
65
+ β”‚ β”œβ”€β”€ seeder.py # Π˜Π΄Π΅ΠΌΠΏΠΎΡ‚Π΅Π½Ρ‚Π½Ρ‹ΠΉ сидинг дСмонстрационных Π΄Π°Π½Π½Ρ‹Ρ…
66
+ β”‚ └── seo.py # ГСнСрация JSON-LD ΠΌΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠΈ ΠΈ Ρ…Π»Π΅Π±Π½Ρ‹Ρ… ΠΊΡ€ΠΎΡˆΠ΅ΠΊ
67
+ └── templates/
68
+ └── forum_topic.html # Π¨Π°Π±Π»ΠΎΠ½ Jinja2 для Dynamic Rendering
69
+ ```
70
+
71
+ ---
72
+
73
+ ## πŸ§ͺ Запуск тСстов
74
+
75
+ ```bash
76
+ pytest
77
+ ```
@@ -0,0 +1,64 @@
1
+ # πŸš€ PyFerro Showcase & Demo Application
2
+
3
+ ΠžΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΠΎΠ΅ дСмонстрационноС ΠΈ ΠΎΠ±ΡƒΡ‡Π°ΡŽΡ‰Π΅Π΅ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π° Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠΌ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ΅ **PyFerro**.
4
+ Π‘Π»ΡƒΠΆΠΈΡ‚ ΠΎΠ±Ρ€Π°Π·Ρ†ΠΎΠΌ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹, ΠΆΠΈΠ²Ρ‹ΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ΠΎΠΌ использования ΠΈ E2E-тСстСром ядра.
5
+
6
+ ---
7
+
8
+ ## πŸ’‘ ΠžΡΠΎΠ±Π΅Π½Π½ΠΎΡΡ‚ΠΈ
9
+
10
+ - **Π˜Π·ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Π½Π½Π°Ρ Π±Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…:** ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ создаСт ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Π»ΠΎΠΊΠ°Π»ΡŒΠ½ΡƒΡŽ Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… `demo.db` (SQLite). Π’Π°ΡˆΠ° корпоративная Π‘Π” остаСтся Π² ΠΏΠΎΠ»Π½ΠΎΠΉ бСзопасности.
11
+ - **ΠŸΠΎΠ»Π½ΠΎΡ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ SEO-Ρ„ΠΎΡ€ΡƒΠΌ:**
12
+ - ДрСвовидная структура Ρ€Π°Π·Π΄Π΅Π»ΠΎΠ² ΠΈ Π²Π΅Ρ‚ΠΎΠΊ (`Topic`, `Message`).
13
+ - ΠœΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠ° Schema.org (`BreadcrumbList`, `DiscussionForumPosting` Π² JSON-LD).
14
+ - ДинамичСский Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³ страниц для поисковых Π±ΠΎΡ‚ΠΎΠ² (Jinja2).
15
+ - **Π‘ΠΈΠΌΠΌΠ΅Ρ‚Ρ€ΠΈΡ‡Π½Ρ‹ΠΉ JSON-RPC 2.0:** ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² с ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π΅ΠΌ Ρ€ΠΎΠ»Π΅ΠΉ (RBAC).
16
+ - **Π“ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ сид-Π΄Π°Π½Π½Ρ‹Π΅:** АвтоматичСскоС Π½Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ 10 Ρ‚Π΅ΠΌΠ°ΠΌΠΈ ΠΈ Π²Π΅Ρ‚ΠΊΠ°ΠΌΠΈ обсуТдСний ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΌ запускС.
17
+
18
+ ---
19
+
20
+ ## ⚑ Быстрый старт
21
+
22
+ ### 1. Установка ΠΈΠ· PyPI:
23
+ ```bash
24
+ pip install pyferro-demo
25
+ ```
26
+
27
+ ### 2. Запуск прилоТСния:
28
+ ```bash
29
+ pyferro-demo run
30
+ ```
31
+ ΠΈΠ»ΠΈ Π±Π΅Π· ΠΏΡ€Π΅Π΄Π²Π°Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΉ установки:
32
+ ```bash
33
+ pipx run pyferro-demo
34
+ ```
35
+
36
+ ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ запустится Π½Π° Rust-сСрвСрС Granian:
37
+ - **Админка PyFerro:** [http://localhost:8080/pyferro_admin/](http://localhost:8080/pyferro_admin/) (Π»ΠΎΠ³ΠΈΠ½: `admin`, ΠΏΠ°Ρ€ΠΎΠ»ΡŒ: `admin`)
38
+ - **JSON-RPC эндпоинт:** `ws://localhost:8080/wsrpc`
39
+ - **Sitemap XML:** [http://localhost:8080/sitemap.xml](http://localhost:8080/sitemap.xml)
40
+
41
+ ---
42
+
43
+ ## πŸ“‚ Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°
44
+
45
+ ```text
46
+ pyferro_demo/
47
+ β”œβ”€β”€ cli.py # Π’ΠΎΡ‡ΠΊΠ° Π²Ρ…ΠΎΠ΄Π° консольной ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ pyferro-demo
48
+ β”œβ”€β”€ app.py # Π€Π°Π±Ρ€ΠΈΠΊΠ° прилоТСния PyFerroApp ΠΈ рСгистрация ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ
49
+ β”œβ”€β”€ forum/
50
+ β”‚ β”œβ”€β”€ models.py # SQLAlchemy-ΠΌΠΎΠ΄Π΅Π»ΠΈ Ρ‚ΠΎΠΏΠΈΠΊΠΎΠ² ΠΈ сообщСний
51
+ β”‚ β”œβ”€β”€ rpc.py # JSON-RPC 2.0 ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ (forum.list_topics, forum.get_topic ΠΈ Ρ‚.Π΄.)
52
+ β”‚ β”œβ”€β”€ seeder.py # Π˜Π΄Π΅ΠΌΠΏΠΎΡ‚Π΅Π½Ρ‚Π½Ρ‹ΠΉ сидинг дСмонстрационных Π΄Π°Π½Π½Ρ‹Ρ…
53
+ β”‚ └── seo.py # ГСнСрация JSON-LD ΠΌΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠΈ ΠΈ Ρ…Π»Π΅Π±Π½Ρ‹Ρ… ΠΊΡ€ΠΎΡˆΠ΅ΠΊ
54
+ └── templates/
55
+ └── forum_topic.html # Π¨Π°Π±Π»ΠΎΠ½ Jinja2 для Dynamic Rendering
56
+ ```
57
+
58
+ ---
59
+
60
+ ## πŸ§ͺ Запуск тСстов
61
+
62
+ ```bash
63
+ pytest
64
+ ```
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyferro-demo"
7
+ version = "0.1.0"
8
+ description = "Showcase & educational demo application built on the PyFerro framework (SEO Forum, JSON-RPC, Granian RSGI)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Alex Titoff", email = "a.tit.off@gmail.com" }
14
+ ]
15
+ keywords = ["pyferro", "demo", "forum", "showcase", "rsgi", "wsrpc"]
16
+ dependencies = [
17
+ "pyferro>=2.1.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ pyferro-demo = "pyferro_demo.cli:main"
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/atitoff-dotcom/PyFerro"
25
+ Repository = "https://github.com/atitoff-dotcom/PyFerro.git"
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
29
+ include = ["pyferro_demo*"]
30
+
31
+ [tool.setuptools.package-data]
32
+ "pyferro_demo" = [
33
+ "templates/*",
34
+ ]
35
+
36
+ [tool.pytest.ini_options]
37
+ pythonpath = [".", "src"]
38
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """
2
+ PyFerro Showcase & Demo Application.
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from pyferro_demo.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,44 @@
1
+ """
2
+ Π€Π°Π±Ρ€ΠΈΠΊΠ° дСмонстрационного прилоТСния PyFerro Demo.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ from pyferro2.db import Database
10
+ from pyferro2.server import PyFerroApp
11
+ from pyferro_demo.forum.models import ForumBase
12
+ from pyferro_demo.forum.rpc import register_forum_rpc
13
+ from pyferro_demo.forum.seeder import seed_forum_data
14
+
15
+
16
+ async def create_demo_app(
17
+ db_url: str = "sqlite+aiosqlite:///demo.db",
18
+ secret_key: str = "pyferro-demo-secret-key-32-chars-long!",
19
+ ) -> PyFerroApp:
20
+ """
21
+ Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ ΠΈ настраиваСт экзСмпляр PyFerroApp с дСмонстрационным Ρ„ΠΎΡ€ΡƒΠΌΠΎΠΌ.
22
+ """
23
+ db = Database(db_url)
24
+ # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ ядра PyFerro (pf_*)
25
+ await db.create_tables()
26
+
27
+ # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ дСмонстрационного Ρ„ΠΎΡ€ΡƒΠΌΠ° Π² Ρ‚ΠΎΠΉ ΠΆΠ΅ ΠΈΠ·ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ demo.db
28
+ async with db.engine.begin() as conn:
29
+ await conn.run_sync(ForumBase.metadata.create_all)
30
+
31
+ # Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ сид-Π΄Π°Π½Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΡƒΠΌΠ°
32
+ async with db.session() as session:
33
+ await seed_forum_data(session)
34
+
35
+ # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ PyFerro
36
+ app = PyFerroApp(
37
+ db=db,
38
+ secret_key=secret_key,
39
+ )
40
+
41
+ # РСгистрируСм RPC-ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ Ρ„ΠΎΡ€ΡƒΠΌΠ°
42
+ register_forum_rpc(app.router, db)
43
+
44
+ return app
@@ -0,0 +1,52 @@
1
+ """
2
+ CLI ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Π° запуска pyferro-demo.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import asyncio
9
+ import sys
10
+
11
+ from pyferro_demo.app import create_demo_app
12
+
13
+
14
+ def main() -> None:
15
+ parser = argparse.ArgumentParser(
16
+ prog="pyferro-demo",
17
+ description="πŸš€ Запуск дСмонстрационного прилоТСния PyFerro (Π€ΠΎΡ€ΡƒΠΌ, JSON-RPC, Granian RSGI)",
18
+ )
19
+ parser.add_argument(
20
+ "command",
21
+ nargs="?",
22
+ default="run",
23
+ choices=["run"],
24
+ help="Команда (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ run)",
25
+ )
26
+ parser.add_argument(
27
+ "--host", default="127.0.0.1", help="Π₯ост сСрвСра (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ 127.0.0.1)"
28
+ )
29
+ parser.add_argument(
30
+ "--port", type=int, default=8080, help="ΠŸΠΎΡ€Ρ‚ сСрвСра (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ 8080)"
31
+ )
32
+ parser.add_argument(
33
+ "--db", default="demo.db", help="ΠŸΡƒΡ‚ΡŒ ΠΊ SQLite Π±Π°Π·Π΅ Π΄Π°Π½Π½Ρ‹Ρ… (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ demo.db)"
34
+ )
35
+
36
+ args = parser.parse_args()
37
+
38
+ print("=" * 60)
39
+ print(" πŸš€ Запуск PyFerro Showcase & Demo Application")
40
+ print(f" πŸ“¦ Π‘Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…: {args.db}")
41
+ print(f" 🌐 АдрСс: http://{args.host}:{args.port}")
42
+ print(f" πŸ›‘οΈ Админка: http://{args.host}:{args.port}/pyferro_admin/")
43
+ print(" πŸ”‘ Π›ΠΎΠ³ΠΈΠ½: admin / ΠŸΠ°Ρ€ΠΎΠ»ΡŒ: admin")
44
+ print("=" * 60)
45
+
46
+ db_url = f"sqlite+aiosqlite:///{args.db}"
47
+ app = asyncio.run(create_demo_app(db_url=db_url))
48
+ app.run(host=args.host, port=args.port)
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,15 @@
1
+ """
2
+ ΠœΠΎΠ΄ΡƒΠ»ΡŒ Ρ„ΠΎΡ€ΡƒΠΌΠ°: ΠΌΠΎΠ΄Π΅Π»ΠΈ, RPC ΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ‚ΠΎΡ€Ρ‹ SEO.
3
+ """
4
+
5
+ from pyferro_demo.forum.models import ForumBase, Topic, Message
6
+ from pyferro_demo.forum.rpc import register_forum_rpc
7
+ from pyferro_demo.forum.seeder import seed_forum_data
8
+
9
+ __all__ = [
10
+ "ForumBase",
11
+ "Topic",
12
+ "Message",
13
+ "register_forum_rpc",
14
+ "seed_forum_data",
15
+ ]
@@ -0,0 +1,70 @@
1
+ """
2
+ SQLAlchemy-ΠΌΠΎΠ΄Π΅Π»ΠΈ дСмонстрационного Ρ„ΠΎΡ€ΡƒΠΌΠ°.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import datetime, timezone
8
+
9
+ from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
10
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
11
+
12
+
13
+ def _now_utc() -> datetime:
14
+ return datetime.now(timezone.utc)
15
+
16
+
17
+ class ForumBase(DeclarativeBase):
18
+ """Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ класс ΠΌΠΎΠ΄Π΅Π»Π΅ΠΉ дСмонстрационного Ρ„ΠΎΡ€ΡƒΠΌΠ°."""
19
+ pass
20
+
21
+
22
+ class Topic(ForumBase):
23
+ """ДрСвовидная иСрархия Ρ€Π°Π·Π΄Π΅Π»ΠΎΠ² ΠΈ Ρ‚Π΅ΠΌ Ρ„ΠΎΡ€ΡƒΠΌΠ°."""
24
+
25
+ __tablename__ = "forum_topics"
26
+
27
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
28
+ parent_id: Mapped[int | None] = mapped_column(
29
+ Integer,
30
+ ForeignKey("forum_topics.id", ondelete="CASCADE"),
31
+ nullable=True,
32
+ index=True,
33
+ )
34
+ title: Mapped[str] = mapped_column(String(200), nullable=False)
35
+ slug: Mapped[str] = mapped_column(String(200), unique=True, nullable=False, index=True)
36
+ is_category: Mapped[bool] = mapped_column(Boolean, default=False)
37
+ seo_title: Mapped[str | None] = mapped_column(String(250), nullable=True)
38
+ seo_description: Mapped[str | None] = mapped_column(Text, nullable=True)
39
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now_utc)
40
+ updated_at: Mapped[datetime] = mapped_column(
41
+ DateTime(timezone=True), default=_now_utc, onupdate=_now_utc
42
+ )
43
+
44
+ # ΠžΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΠ΅ ΠΊ сообщСниям
45
+ messages: Mapped[list[Message]] = relationship(
46
+ "Message",
47
+ back_populates="topic",
48
+ cascade="all, delete-orphan",
49
+ order_by="Message.created_at",
50
+ )
51
+
52
+
53
+ class Message(ForumBase):
54
+ """БообщСния Π²Π½ΡƒΡ‚Ρ€ΠΈ Ρ„ΠΎΡ€ΡƒΠΌΠ½ΠΎΠΉ Π²Π΅Ρ‚ΠΊΠΈ."""
55
+
56
+ __tablename__ = "forum_messages"
57
+
58
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
59
+ topic_id: Mapped[int] = mapped_column(
60
+ Integer,
61
+ ForeignKey("forum_topics.id", ondelete="CASCADE"),
62
+ nullable=False,
63
+ index=True,
64
+ )
65
+ author_name: Mapped[str] = mapped_column(String(120), nullable=False)
66
+ content: Mapped[str] = mapped_column(Text, nullable=False)
67
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now_utc)
68
+
69
+ # ΠžΠ±Ρ€Π°Ρ‚Π½ΠΎΠ΅ ΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΠ΅
70
+ topic: Mapped[Topic] = relationship("Topic", back_populates="messages")
@@ -0,0 +1,108 @@
1
+ """
2
+ JSON-RPC 2.0 интСрфСйс Ρ„ΠΎΡ€ΡƒΠΌΠ° (forum.*).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from sqlalchemy import select
10
+ from sqlalchemy.orm import selectinload
11
+
12
+ from pyferro2.core.session_data import SessionData
13
+ from pyferro2.db.engine import Database
14
+ from pyferro2.rpc.router import RpcRouter
15
+ from pyferro_demo.forum.models import Message, Topic
16
+ from pyferro_demo.forum.seo import get_topic_breadcrumbs, get_topic_discussion_posting
17
+
18
+
19
+ def register_forum_rpc(router: RpcRouter, db: Database) -> None:
20
+ """РСгистрируСт ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ `forum.*` Π² RPC-Ρ€ΠΎΡƒΡ‚Π΅Ρ€Π΅ PyFerro."""
21
+
22
+ class forum:
23
+ @router.rpc_method(public=True)
24
+ async def list_topics(
25
+ session_data: SessionData, parent_id: int | None = None
26
+ ) -> list[dict[str, Any]]:
27
+ """Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ список Ρ‚Π΅ΠΌ Ρ„ΠΎΡ€ΡƒΠΌΠ°."""
28
+ async with db.session() as session:
29
+ stmt = select(Topic).where(Topic.parent_id == parent_id)
30
+ res = await session.execute(stmt)
31
+ topics = res.scalars().all()
32
+ return [
33
+ {
34
+ "id": t.id,
35
+ "parent_id": t.parent_id,
36
+ "title": t.title,
37
+ "slug": t.slug,
38
+ "is_category": t.is_category,
39
+ "seo_title": t.seo_title,
40
+ "seo_description": t.seo_description,
41
+ "created_at": t.created_at.isoformat(),
42
+ }
43
+ for t in topics
44
+ ]
45
+
46
+ @router.rpc_method(public=True)
47
+ async def get_topic(
48
+ session_data: SessionData, slug: str
49
+ ) -> dict[str, Any] | None:
50
+ """Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Ρ‚Π΅ΠΌΡƒ ΠΈ сообщСния ΠΏΠΎ slug."""
51
+ async with db.session() as session:
52
+ stmt = (
53
+ select(Topic)
54
+ .where(Topic.slug == slug)
55
+ .options(selectinload(Topic.messages))
56
+ )
57
+ res = await session.execute(stmt)
58
+ topic = res.scalar_one_or_none()
59
+ if not topic:
60
+ return None
61
+
62
+ breadcrumbs = await get_topic_breadcrumbs(session, topic.id)
63
+ discussion_ld = await get_topic_discussion_posting(session, topic.id)
64
+
65
+ return {
66
+ "id": topic.id,
67
+ "title": topic.title,
68
+ "slug": topic.slug,
69
+ "is_category": topic.is_category,
70
+ "seo_title": topic.seo_title,
71
+ "seo_description": topic.seo_description,
72
+ "breadcrumbs": breadcrumbs,
73
+ "json_ld": discussion_ld,
74
+ "messages": [
75
+ {
76
+ "id": m.id,
77
+ "author_name": m.author_name,
78
+ "content": m.content,
79
+ "created_at": m.created_at.isoformat(),
80
+ }
81
+ for m in topic.messages
82
+ ],
83
+ }
84
+
85
+ @router.rpc_method(role=["user", "admin"])
86
+ async def create_message(
87
+ session_data: SessionData,
88
+ topic_id: int,
89
+ content: str,
90
+ author_name: str | None = None,
91
+ ) -> dict[str, Any]:
92
+ """Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ Π½ΠΎΠ²ΠΎΠ΅ сообщСниС Π² Ρ‚Π΅ΠΌΠ΅ (доступно Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹ΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌ)."""
93
+ name = author_name or session_data.username or "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ"
94
+ async with db.session() as session:
95
+ msg = Message(
96
+ topic_id=topic_id,
97
+ author_name=name,
98
+ content=content,
99
+ )
100
+ session.add(msg)
101
+ await session.commit()
102
+ return {
103
+ "id": msg.id,
104
+ "topic_id": msg.topic_id,
105
+ "author_name": msg.author_name,
106
+ "content": msg.content,
107
+ "created_at": msg.created_at.isoformat(),
108
+ }
@@ -0,0 +1,184 @@
1
+ """
2
+ Π”Π΅ΠΌΠΎ-сидСр для Ρ„ΠΎΡ€ΡƒΠΌΠ°: Π½Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Ρ€Π°Π·Π΄Π΅Π»Π°ΠΌΠΈ ΠΈ сообщСниями.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import datetime, timedelta, timezone
8
+
9
+ from sqlalchemy import select
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from pyferro_demo.forum.models import Message, Topic
13
+
14
+
15
+ async def seed_forum_data(session: AsyncSession) -> dict[str, int]:
16
+ """
17
+ Π˜Π΄Π΅ΠΌΠΏΠΎΡ‚Π΅Π½Ρ‚Π½ΠΎ заполняСт Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… Ρ‚Π΅ΠΌΠ°ΠΌΠΈ ΠΈ сообщСниями, Ссли ΠΎΠ½Π° пуста.
18
+ """
19
+ res = await session.execute(select(Topic))
20
+ if res.scalars().first() is not None:
21
+ return {"topics": 0, "messages": 0}
22
+
23
+ now = datetime.now(timezone.utc)
24
+
25
+ # 1. ΠšΠΎΡ€Π½Π΅Π²ΠΎΠΉ Ρ€Π°Π·Π΄Π΅Π» 1
26
+ t1 = Topic(
27
+ title="ΠžΠ±Ρ‰ΠΈΠ΅ дискуссии",
28
+ slug="general",
29
+ is_category=True,
30
+ seo_title="ΠžΠ±Ρ‰ΠΈΠ΅ Ρ‚Π΅ΠΌΡ‹ ΠΈ новости сообщСства PyFerro",
31
+ seo_description="ΠšΠΎΡ€Π½Π΅Π²ΠΎΠΉ Ρ€Π°Π·Π΄Π΅Π» ΠΎΠ±Ρ‰ΠΈΡ… обсуТдСний, анонсов ΠΈ ΠΏΡ€Π°Π²ΠΈΠ» ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° PyFerro.",
32
+ created_at=now - timedelta(days=10),
33
+ )
34
+ session.add(t1)
35
+ await session.flush()
36
+
37
+ # 2. ΠŸΠΎΠ΄Ρ€Π°Π·Π΄Π΅Π» новостСй (Π²Π½ΡƒΡ‚Ρ€ΠΈ 1)
38
+ t2 = Topic(
39
+ parent_id=t1.id,
40
+ title="Новости ΠΈ анонсы",
41
+ slug="news",
42
+ is_category=True,
43
+ seo_title="ΠžΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½Ρ‹Π΅ новости ΠΈ Ρ€Π΅Π»ΠΈΠ·Ρ‹",
44
+ seo_description="ВсС ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½Ρ‹Π΅ новости Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ°, Ρ€Π΅Π»ΠΈΠ·-Π½ΠΎΡ‚Ρ‹ ΠΈ Π²Π°ΠΆΠ½Ρ‹Π΅ измСнСния.",
45
+ created_at=now - timedelta(days=9),
46
+ )
47
+ session.add(t2)
48
+ await session.flush()
49
+
50
+ # 3. Π’Π΅Ρ‚ΠΊΠ° обсуТдСния Ρ€Π΅Π»ΠΈΠ·Π° (Π²Π½ΡƒΡ‚Ρ€ΠΈ 2)
51
+ t3 = Topic(
52
+ parent_id=t2.id,
53
+ title="Π Π΅Π»ΠΈΠ· PyFerro 2.0: Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° ΠΈ возмоТности",
54
+ slug="pyferro-2-0-release",
55
+ is_category=False,
56
+ seo_title="ΠžΠ±ΡΡƒΠΆΠ΄Π΅Π½ΠΈΠ΅ Ρ€Π΅Π»ΠΈΠ·Π° PyFerro 2.0",
57
+ seo_description="ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Ρ‹ΠΉ Ρ€Π°Π·Π±ΠΎΡ€ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ PyFerro 2.0: Rust-сСрвСр Granian, RSGI, JSON-RPC ΠΈ Svelte 5.",
58
+ created_at=now - timedelta(days=8),
59
+ )
60
+ session.add(t3)
61
+
62
+ # 4. Π’Π΅Ρ‚ΠΊΠ° Π΄ΠΎΡ€ΠΎΠΆΠ½ΠΎΠΉ ΠΊΠ°Ρ€Ρ‚Ρ‹ (Π²Π½ΡƒΡ‚Ρ€ΠΈ 2)
63
+ t4 = Topic(
64
+ parent_id=t2.id,
65
+ title="ДороТная ΠΊΠ°Ρ€Ρ‚Π° развития Π½Π° 2026 Π³ΠΎΠ΄",
66
+ slug="roadmap-2026",
67
+ is_category=False,
68
+ seo_title="Roadmap PyFerro 2026",
69
+ seo_description="ΠŸΠ»Π°Π½Ρ‹ развития ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΡ‹: ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° Π½ΠΎΠ²Ρ‹Ρ… ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»ΠΎΠ², ΠΏΠ»Π°Π³ΠΈΠ½Ρ‹ ΠΈ ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅.",
70
+ created_at=now - timedelta(days=7),
71
+ )
72
+ session.add(t4)
73
+
74
+ # 5. Π’Π΅ΠΌΠ° ΠΏΡ€Π°Π²ΠΈΠ» (Π²Π½ΡƒΡ‚Ρ€ΠΈ 1)
75
+ t5 = Topic(
76
+ parent_id=t1.id,
77
+ title="ΠŸΡ€Π°Π²ΠΈΠ»Π° Ρ„ΠΎΡ€ΡƒΠΌΠ° ΠΈ оформлСния ΠΊΠΎΠ΄Π°",
78
+ slug="rules",
79
+ is_category=False,
80
+ seo_title="ΠŸΡ€Π°Π²ΠΈΠ»Π° сообщСства PyFerro",
81
+ seo_description="Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ ΠΏΠΎ созданию Π²Π΅Ρ‚ΠΎΠΊ, ΠΊΡƒΠ»ΡŒΡ‚ΡƒΡ€Π΅ общСния ΠΈ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡŽ сниппСтов ΠΊΠΎΠ΄Π°.",
82
+ created_at=now - timedelta(days=6),
83
+ )
84
+ session.add(t5)
85
+ await session.flush()
86
+
87
+ # 6. ΠšΠΎΡ€Π½Π΅Π²ΠΎΠΉ Ρ€Π°Π·Π΄Π΅Π» 2: Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°
88
+ t6 = Topic(
89
+ title="Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΈ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π°",
90
+ slug="development",
91
+ is_category=True,
92
+ seo_title="Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° бэкСнда ΠΈ интСрфСйсов",
93
+ seo_description="ВСхничСский Ρ€Π°Π·Π΄Π΅Π»: Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° сСрвисов, Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…, API ΠΈ Ρ„Ρ€ΠΎΠ½Ρ‚Π΅Π½Π΄.",
94
+ created_at=now - timedelta(days=5),
95
+ )
96
+ session.add(t6)
97
+ await session.flush()
98
+
99
+ # 7. ΠŸΠΎΠ΄Ρ€Π°Π·Π΄Π΅Π» бэкСнда (Π²Π½ΡƒΡ‚Ρ€ΠΈ 6)
100
+ t7 = Topic(
101
+ parent_id=t6.id,
102
+ title="БэкСнд ΠΈ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…",
103
+ slug="backend-db",
104
+ is_category=True,
105
+ seo_title="БэкСнд Π½Π° Rust RSGI ΠΈ SQLAlchemy",
106
+ seo_description="ΠžΠ±ΡΡƒΠΆΠ΄Π΅Π½ΠΈΠ΅ асинхронного Π΄Π²ΠΈΠΆΠΊΠ°, ΠΌΠΎΠ΄Π΅Π»Π΅ΠΉ SQLAlchemy ΠΈ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ.",
107
+ created_at=now - timedelta(days=4),
108
+ )
109
+ session.add(t7)
110
+ await session.flush()
111
+
112
+ # 8. Π’ΠΎΠΏΠΈΠΊ ΠΎ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ Granian (Π²Π½ΡƒΡ‚Ρ€ΠΈ 7)
113
+ t8 = Topic(
114
+ parent_id=t7.id,
115
+ title="Π‘Π΅Π½Ρ‡ΠΌΠ°Ρ€ΠΊΠΈ Granian RSGI ΠΏΡ€ΠΎΡ‚ΠΈΠ² Uvicorn",
116
+ slug="granian-vs-uvicorn",
117
+ is_category=False,
118
+ seo_title="Π‘Ρ€Π°Π²Π½Π΅Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ Granian ΠΈ Uvicorn",
119
+ seo_description="ВСсты RPS ΠΈ Π·Π°Π΄Π΅Ρ€ΠΆΠ΅ΠΊ: ΠΊΠ°ΠΊ Rust-Ρ€Π°Π½Ρ‚Π°ΠΉΠΌ Granian ускоряСт асинхронный Python.",
120
+ created_at=now - timedelta(days=3),
121
+ )
122
+ session.add(t8)
123
+
124
+ # 9. Π’ΠΎΠΏΠΈΠΊ ΠΎ сТатии Tabular RPC (Π²Π½ΡƒΡ‚Ρ€ΠΈ 7)
125
+ t9 = Topic(
126
+ parent_id=t7.id,
127
+ title="Π‘ΠΆΠ°Ρ‚ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… Tabular: экономия сСтСвого Ρ‚Ρ€Π°Ρ„ΠΈΠΊΠ° Π΄ΠΎ 80%",
128
+ slug="tabular-compression",
129
+ is_category=False,
130
+ seo_title="Экономия Ρ‚Ρ€Π°Ρ„ΠΈΠΊΠ° с Tabular RPC",
131
+ seo_description="Π Π°Π·Π±ΠΎΡ€ ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π° Tabular для ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ списков словарСй Ρ‡Π΅Ρ€Π΅Π· WebSocket.",
132
+ created_at=now - timedelta(days=2),
133
+ )
134
+ session.add(t9)
135
+
136
+ # 10. Π’ΠΎΠΏΠΈΠΊ ΠΎ SEO ΠΈ Dynamic Rendering (Π²Π½ΡƒΡ‚Ρ€ΠΈ 6)
137
+ t10 = Topic(
138
+ parent_id=t6.id,
139
+ title="Dynamic Rendering: идСальная индСксация SPA для Google ΠΈ ЯндСкса",
140
+ slug="dynamic-rendering-guide",
141
+ is_category=False,
142
+ seo_title="Руководство ΠΏΠΎ Dynamic Rendering Π² PyFerro",
143
+ seo_description="ΠŸΡ€Π°ΠΊΡ‚ΠΈΠΊΠ° настройки Jinja2-шаблонов, IndexNow ΠΈ ΠΊΠ°Ρ€Ρ‚Ρ‹ сайта sitemap.xml для SPA.",
144
+ created_at=now - timedelta(days=1),
145
+ )
146
+ session.add(t10)
147
+ await session.flush()
148
+
149
+ # НаполняСм сообщСниями
150
+ m1 = Message(
151
+ topic_id=t3.id,
152
+ author_name="АлСксСй (АрхитСктор)",
153
+ content="КоллСги, Ρ€Π°Π΄ ΠΏΡ€Π΅Π΄ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ PyFerro 2.0! ΠŸΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ° ΠΏΠ΅Ρ€Π΅Π²Π΅Π΄Π΅Π½Π° Π½Π° Rust RSGI (Granian), Ρ‡Ρ‚ΠΎ снизило Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ Π΄ΠΎ микросСкунд.",
154
+ created_at=now - timedelta(days=8, hours=2),
155
+ )
156
+ m2 = Message(
157
+ topic_id=t3.id,
158
+ author_name="Π”ΠΌΠΈΡ‚Ρ€ΠΈΠΉ (DevOps)",
159
+ content="ΠžΡ‚Π»ΠΈΡ‡Π½Ρ‹ΠΉ Ρ€Π΅Π»ΠΈΠ·! ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΠ»ΠΈ сборку Docker-ΠΎΠ±Ρ€Π°Π·ΠΎΠ² β€” вСс ΠΌΠΈΠ½ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ, ΠΏΠΎΡ‚Ρ€Π΅Π±Π»Π΅Π½ΠΈΠ΅ памяти снизилось Π½Π° 40%.",
160
+ created_at=now - timedelta(days=7, hours=18),
161
+ )
162
+ m3 = Message(
163
+ topic_id=t8.id,
164
+ author_name="Иван (Senior Python)",
165
+ content="Π’ синтСтичСских тСстах Granian Π΄Π΅Ρ€ΠΆΠΈΡ‚ 45k RPS Π½Π° ΠΎΠ΄Π½ΠΎΠΌ ядрС ΠΏΡ€ΠΈ Π½ΡƒΠ»Π΅Π²ΠΎΠΌ Π΄Ρ€ΠΎΠΏΠ΅ ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠ². Π­Ρ‚ΠΎ Π»ΡƒΡ‡ΡˆΠΈΠΉ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Π² Python экосистСмС.",
166
+ created_at=now - timedelta(days=3, hours=1),
167
+ )
168
+ m4 = Message(
169
+ topic_id=t9.id,
170
+ author_name="ΠœΠ°Ρ€ΠΈΡ (Frontend)",
171
+ content="Tabular Data Compression сработал ΠΎΡ‚Π»ΠΈΡ‡Π½ΠΎ Π² Π³Ρ€ΠΈΠ΄Π°Ρ… Π°Π΄ΠΌΠΈΠ½ΠΊΠΈ: Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° 10,000 строк Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π·Π°Π½ΠΈΠΌΠ°Π΅Ρ‚ миллисСкунды.",
172
+ created_at=now - timedelta(days=2, hours=3),
173
+ )
174
+ m5 = Message(
175
+ topic_id=t10.id,
176
+ author_name="Π‘Π΅Ρ€Π³Π΅ΠΉ (SEO-спСциалист)",
177
+ content="Π‘ΠΎΡ‚Ρ‹ поисковиков видят чистый HTML с Ρ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ Schema.org, Π° ΠΆΠΈΠ²Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°ΡŽΡ‚ Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹ΠΉ SPA. Π˜Π΄Π΅Π°Π»ΡŒΠ½Ρ‹ΠΉ баланс!",
178
+ created_at=now - timedelta(days=1, hours=4),
179
+ )
180
+
181
+ session.add_all([m1, m2, m3, m4, m5])
182
+ await session.commit()
183
+
184
+ return {"topics": 10, "messages": 5}
@@ -0,0 +1,122 @@
1
+ """
2
+ Π“Π΅Π½Π΅Ρ€Π°Ρ‚ΠΎΡ€ SEO-ΠΌΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠΈ Schema.org (BreadcrumbList, DiscussionForumPosting) ΠΈ sitemap.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from sqlalchemy import select
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+ from sqlalchemy.orm import selectinload
12
+
13
+ from pyferro_demo.forum.models import Message, Topic
14
+
15
+
16
+ async def get_topic_breadcrumbs(
17
+ session: AsyncSession, topic_id: int, base_url: str = "http://localhost:8080"
18
+ ) -> dict[str, Any]:
19
+ """Π‘Ρ‚Ρ€ΠΎΠΈΡ‚ Ρ…Π»Π΅Π±Π½Ρ‹Π΅ ΠΊΡ€ΠΎΡˆΠΊΠΈ Schema.org BreadcrumbList."""
20
+ base_url = base_url.rstrip("/")
21
+ chain: list[Topic] = []
22
+ curr_id: int | None = topic_id
23
+ visited: set[int] = set()
24
+
25
+ while curr_id is not None and curr_id not in visited:
26
+ visited.add(curr_id)
27
+ topic = await session.get(Topic, curr_id)
28
+ if not topic:
29
+ break
30
+ chain.append(topic)
31
+ curr_id = topic.parent_id
32
+
33
+ chain.reverse()
34
+
35
+ path_segments: list[str] = []
36
+ items: list[dict[str, Any]] = [
37
+ {
38
+ "@type": "ListItem",
39
+ "position": 1,
40
+ "name": "Π€ΠΎΡ€ΡƒΠΌ",
41
+ "item": f"{base_url}/forum",
42
+ }
43
+ ]
44
+
45
+ for idx, t in enumerate(chain, start=2):
46
+ path_segments.append(t.slug)
47
+ full_url = f"{base_url}/forum/{'/'.join(path_segments)}"
48
+ items.append(
49
+ {
50
+ "@type": "ListItem",
51
+ "position": idx,
52
+ "name": t.title,
53
+ "item": full_url,
54
+ }
55
+ )
56
+
57
+ return {
58
+ "@context": "https://schema.org",
59
+ "@type": "BreadcrumbList",
60
+ "itemListElement": items,
61
+ }
62
+
63
+
64
+ async def get_topic_discussion_posting(
65
+ session: AsyncSession, topic_id: int, base_url: str = "http://localhost:8080"
66
+ ) -> dict[str, Any] | None:
67
+ """Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΌΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΡƒ DiscussionForumPosting для сниппСтов поисковиков."""
68
+ base_url = base_url.rstrip("/")
69
+ stmt = (
70
+ select(Topic)
71
+ .where(Topic.id == topic_id)
72
+ .options(selectinload(Topic.messages))
73
+ )
74
+ result = await session.execute(stmt)
75
+ topic = result.scalar_one_or_none()
76
+
77
+ if not topic:
78
+ return None
79
+
80
+ messages = sorted(topic.messages, key=lambda m: m.created_at)
81
+ first_post = messages[0] if messages else None
82
+ replies = messages[1:] if len(messages) > 1 else []
83
+
84
+ url = f"{base_url}/forum/{topic.slug}"
85
+
86
+ ld: dict[str, Any] = {
87
+ "@context": "https://schema.org",
88
+ "@type": "DiscussionForumPosting",
89
+ "headline": topic.seo_title or topic.title,
90
+ "description": topic.seo_description or topic.title,
91
+ "url": url,
92
+ "datePublished": topic.created_at.isoformat(),
93
+ "dateModified": topic.updated_at.isoformat(),
94
+ "interactionStatistic": {
95
+ "@type": "InteractionCounter",
96
+ "interactionType": "https://schema.org/CommentAction",
97
+ "userInteractionCount": len(topic.messages),
98
+ },
99
+ }
100
+
101
+ if first_post:
102
+ ld["author"] = {
103
+ "@type": "Person",
104
+ "name": first_post.author_name,
105
+ }
106
+ ld["articleBody"] = first_post.content
107
+
108
+ if replies:
109
+ ld["comment"] = [
110
+ {
111
+ "@type": "Comment",
112
+ "text": rep.content,
113
+ "dateCreated": rep.created_at.isoformat(),
114
+ "author": {
115
+ "@type": "Person",
116
+ "name": rep.author_name,
117
+ },
118
+ }
119
+ for rep in replies
120
+ ]
121
+
122
+ return ld
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyferro-demo
3
+ Version: 0.1.0
4
+ Summary: Showcase & educational demo application built on the PyFerro framework (SEO Forum, JSON-RPC, Granian RSGI).
5
+ Author-email: Alex Titoff <a.tit.off@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/atitoff-dotcom/PyFerro
8
+ Project-URL: Repository, https://github.com/atitoff-dotcom/PyFerro.git
9
+ Keywords: pyferro,demo,forum,showcase,rsgi,wsrpc
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pyferro>=2.1.0
13
+
14
+ # πŸš€ PyFerro Showcase & Demo Application
15
+
16
+ ΠžΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΠΎΠ΅ дСмонстрационноС ΠΈ ΠΎΠ±ΡƒΡ‡Π°ΡŽΡ‰Π΅Π΅ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π° Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠΌ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ΅ **PyFerro**.
17
+ Π‘Π»ΡƒΠΆΠΈΡ‚ ΠΎΠ±Ρ€Π°Π·Ρ†ΠΎΠΌ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹, ΠΆΠΈΠ²Ρ‹ΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ΠΎΠΌ использования ΠΈ E2E-тСстСром ядра.
18
+
19
+ ---
20
+
21
+ ## πŸ’‘ ΠžΡΠΎΠ±Π΅Π½Π½ΠΎΡΡ‚ΠΈ
22
+
23
+ - **Π˜Π·ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Π½Π½Π°Ρ Π±Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…:** ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ создаСт ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Π»ΠΎΠΊΠ°Π»ΡŒΠ½ΡƒΡŽ Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… `demo.db` (SQLite). Π’Π°ΡˆΠ° корпоративная Π‘Π” остаСтся Π² ΠΏΠΎΠ»Π½ΠΎΠΉ бСзопасности.
24
+ - **ΠŸΠΎΠ»Π½ΠΎΡ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ SEO-Ρ„ΠΎΡ€ΡƒΠΌ:**
25
+ - ДрСвовидная структура Ρ€Π°Π·Π΄Π΅Π»ΠΎΠ² ΠΈ Π²Π΅Ρ‚ΠΎΠΊ (`Topic`, `Message`).
26
+ - ΠœΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠ° Schema.org (`BreadcrumbList`, `DiscussionForumPosting` Π² JSON-LD).
27
+ - ДинамичСский Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³ страниц для поисковых Π±ΠΎΡ‚ΠΎΠ² (Jinja2).
28
+ - **Π‘ΠΈΠΌΠΌΠ΅Ρ‚Ρ€ΠΈΡ‡Π½Ρ‹ΠΉ JSON-RPC 2.0:** ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² с ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π΅ΠΌ Ρ€ΠΎΠ»Π΅ΠΉ (RBAC).
29
+ - **Π“ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ сид-Π΄Π°Π½Π½Ρ‹Π΅:** АвтоматичСскоС Π½Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ 10 Ρ‚Π΅ΠΌΠ°ΠΌΠΈ ΠΈ Π²Π΅Ρ‚ΠΊΠ°ΠΌΠΈ обсуТдСний ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΌ запускС.
30
+
31
+ ---
32
+
33
+ ## ⚑ Быстрый старт
34
+
35
+ ### 1. Установка ΠΈΠ· PyPI:
36
+ ```bash
37
+ pip install pyferro-demo
38
+ ```
39
+
40
+ ### 2. Запуск прилоТСния:
41
+ ```bash
42
+ pyferro-demo run
43
+ ```
44
+ ΠΈΠ»ΠΈ Π±Π΅Π· ΠΏΡ€Π΅Π΄Π²Π°Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΉ установки:
45
+ ```bash
46
+ pipx run pyferro-demo
47
+ ```
48
+
49
+ ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ запустится Π½Π° Rust-сСрвСрС Granian:
50
+ - **Админка PyFerro:** [http://localhost:8080/pyferro_admin/](http://localhost:8080/pyferro_admin/) (Π»ΠΎΠ³ΠΈΠ½: `admin`, ΠΏΠ°Ρ€ΠΎΠ»ΡŒ: `admin`)
51
+ - **JSON-RPC эндпоинт:** `ws://localhost:8080/wsrpc`
52
+ - **Sitemap XML:** [http://localhost:8080/sitemap.xml](http://localhost:8080/sitemap.xml)
53
+
54
+ ---
55
+
56
+ ## πŸ“‚ Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°
57
+
58
+ ```text
59
+ pyferro_demo/
60
+ β”œβ”€β”€ cli.py # Π’ΠΎΡ‡ΠΊΠ° Π²Ρ…ΠΎΠ΄Π° консольной ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ pyferro-demo
61
+ β”œβ”€β”€ app.py # Π€Π°Π±Ρ€ΠΈΠΊΠ° прилоТСния PyFerroApp ΠΈ рСгистрация ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ
62
+ β”œβ”€β”€ forum/
63
+ β”‚ β”œβ”€β”€ models.py # SQLAlchemy-ΠΌΠΎΠ΄Π΅Π»ΠΈ Ρ‚ΠΎΠΏΠΈΠΊΠΎΠ² ΠΈ сообщСний
64
+ β”‚ β”œβ”€β”€ rpc.py # JSON-RPC 2.0 ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ (forum.list_topics, forum.get_topic ΠΈ Ρ‚.Π΄.)
65
+ β”‚ β”œβ”€β”€ seeder.py # Π˜Π΄Π΅ΠΌΠΏΠΎΡ‚Π΅Π½Ρ‚Π½Ρ‹ΠΉ сидинг дСмонстрационных Π΄Π°Π½Π½Ρ‹Ρ…
66
+ β”‚ └── seo.py # ГСнСрация JSON-LD ΠΌΠΈΠΊΡ€ΠΎΡ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠΈ ΠΈ Ρ…Π»Π΅Π±Π½Ρ‹Ρ… ΠΊΡ€ΠΎΡˆΠ΅ΠΊ
67
+ └── templates/
68
+ └── forum_topic.html # Π¨Π°Π±Π»ΠΎΠ½ Jinja2 для Dynamic Rendering
69
+ ```
70
+
71
+ ---
72
+
73
+ ## πŸ§ͺ Запуск тСстов
74
+
75
+ ```bash
76
+ pytest
77
+ ```
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/pyferro_demo/__init__.py
4
+ src/pyferro_demo/__main__.py
5
+ src/pyferro_demo/app.py
6
+ src/pyferro_demo/cli.py
7
+ src/pyferro_demo.egg-info/PKG-INFO
8
+ src/pyferro_demo.egg-info/SOURCES.txt
9
+ src/pyferro_demo.egg-info/dependency_links.txt
10
+ src/pyferro_demo.egg-info/entry_points.txt
11
+ src/pyferro_demo.egg-info/requires.txt
12
+ src/pyferro_demo.egg-info/top_level.txt
13
+ src/pyferro_demo/forum/__init__.py
14
+ src/pyferro_demo/forum/models.py
15
+ src/pyferro_demo/forum/rpc.py
16
+ src/pyferro_demo/forum/seeder.py
17
+ src/pyferro_demo/forum/seo.py
18
+ tests/test_forum_e2e.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyferro-demo = pyferro_demo.cli:main
@@ -0,0 +1 @@
1
+ pyferro>=2.1.0
@@ -0,0 +1 @@
1
+ pyferro_demo
@@ -0,0 +1,60 @@
1
+ """
2
+ E2E тСсты дСмонстрационного прилоТСния pyferro-demo.
3
+ ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΡŽΡ‚ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ с ядром PyFerro, Ρ€Π°Π±ΠΎΡ‚Ρƒ ΠΌΠΎΠ΄Π΅Π»Π΅ΠΉ, RPC ΠΈ SEO.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import pytest
9
+
10
+ from pyferro2.core.session import Session
11
+ from pyferro2.core.session_data import SessionData
12
+ from pyferro_demo.app import create_demo_app
13
+
14
+
15
+ @pytest.fixture
16
+ async def app():
17
+ """Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ тСстовоС Π΄Π΅ΠΌΠΎ-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π² памяти."""
18
+ demo_app = await create_demo_app(db_url="sqlite+aiosqlite:///:memory:")
19
+ yield demo_app
20
+ await demo_app.db.close()
21
+
22
+
23
+ class TestForumE2E:
24
+ @pytest.mark.asyncio
25
+ async def test_list_topics(self, app):
26
+ """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ списка Ρ‚Π΅ΠΌ Ρ„ΠΎΡ€ΡƒΠΌΠ° Ρ‡Π΅Ρ€Π΅Π· RPC."""
27
+ session = Session(session_id="s-admin", data=SessionData(user_id=1, username="admin", roles=frozenset(["admin"])))
28
+ topics = await app.router.call("forum.list_topics", session)
29
+ assert isinstance(topics, list)
30
+ assert len(topics) >= 2 # ΠšΠΎΡ€Π½Π΅Π²Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_get_topic_with_seo(self, app):
34
+ """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ‚ΠΎΠΏΠΈΠΊΠ° с JSON-LD Ρ€Π°Π·ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ."""
35
+ session = Session(session_id="s-admin", data=SessionData(user_id=1, username="admin", roles=frozenset(["admin"])))
36
+ topic = await app.router.call("forum.get_topic", session, {"slug": "pyferro-2-0-release"})
37
+ assert topic is not None
38
+ assert topic["slug"] == "pyferro-2-0-release"
39
+ assert len(topic["messages"]) >= 1
40
+
41
+ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ…Π»Π΅Π±Π½Ρ‹Ρ… ΠΊΡ€ΠΎΡˆΠ΅ΠΊ
42
+ assert "breadcrumbs" in topic
43
+ assert topic["breadcrumbs"]["@type"] == "BreadcrumbList"
44
+
45
+ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° DiscussionForumPosting
46
+ assert "json_ld" in topic
47
+ assert topic["json_ld"]["@type"] == "DiscussionForumPosting"
48
+ assert "headline" in topic["json_ld"]
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_create_message(self, app):
52
+ """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΡƒ сообщСния Π² Ρ‚Π΅ΠΌΡƒ Ρ‡Π΅Ρ€Π΅Π· RPC."""
53
+ session = Session(session_id="s-user", data=SessionData(user_id=1, username="tester", roles=frozenset(["user"])))
54
+ res = await app.router.call(
55
+ "forum.create_message",
56
+ session,
57
+ {"topic_id": 3, "content": "ВСстовоС сообщСниС ΠΈΠ· E2E тСста"},
58
+ )
59
+ assert res["content"] == "ВСстовоС сообщСниС ΠΈΠ· E2E тСста"
60
+ assert res["author_name"] == "tester"