tina4-python 3.10.34__tar.gz → 3.10.39__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.
- {tina4_python-3.10.34 → tina4_python-3.10.39}/.gitignore +0 -3
- {tina4_python-3.10.34 → tina4_python-3.10.39}/PKG-INFO +1 -1
- {tina4_python-3.10.34 → tina4_python-3.10.39}/pyproject.toml +1 -1
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/__init__.py +1 -1
- tina4_python-3.10.39/tina4_python/ai/__init__.py +312 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/auth/__init__.py +47 -6
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/cli/__init__.py +10 -33
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/router.py +7 -2
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/server.py +3 -3
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/dev_admin/__init__.py +322 -8
- tina4_python-3.10.39/tina4_python/dev_admin/metrics.py +513 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/orm/model.py +12 -2
- tina4_python-3.10.34/tina4_python/ai/__init__.py +0 -412
- {tina4_python-3.10.34 → tina4_python-3.10.39}/README.md +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/request.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Tina4 AI — Install AI coding assistant context files.
|
|
2
|
+
"""
|
|
3
|
+
Simple menu-driven installer for AI tool context files.
|
|
4
|
+
The user picks which tools they use, we install the appropriate files.
|
|
5
|
+
|
|
6
|
+
from tina4_python.ai import show_menu, install_selected
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Ordered list of supported AI tools
|
|
15
|
+
AI_TOOLS = [
|
|
16
|
+
{"name": "claude-code", "description": "Claude Code", "context_file": "CLAUDE.md", "config_dir": ".claude"},
|
|
17
|
+
{"name": "cursor", "description": "Cursor", "context_file": ".cursorules", "config_dir": ".cursor"},
|
|
18
|
+
{"name": "copilot", "description": "GitHub Copilot", "context_file": ".github/copilot-instructions.md", "config_dir": ".github"},
|
|
19
|
+
{"name": "windsurf", "description": "Windsurf", "context_file": ".windsurfrules", "config_dir": None},
|
|
20
|
+
{"name": "aider", "description": "Aider", "context_file": "CONVENTIONS.md", "config_dir": None},
|
|
21
|
+
{"name": "cline", "description": "Cline", "context_file": ".clinerules", "config_dir": None},
|
|
22
|
+
{"name": "codex", "description": "OpenAI Codex", "context_file": "AGENTS.md", "config_dir": None},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_installed(root: str, tool: dict) -> bool:
|
|
27
|
+
"""Check if a tool's context file already exists."""
|
|
28
|
+
return (Path(root).resolve() / tool["context_file"]).exists()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def show_menu(root: str = ".") -> str:
|
|
32
|
+
"""Print the numbered menu and return user input."""
|
|
33
|
+
root = str(Path(root).resolve())
|
|
34
|
+
green = "\033[32m"
|
|
35
|
+
reset = "\033[0m"
|
|
36
|
+
|
|
37
|
+
print("\n Tina4 AI Context Installer\n")
|
|
38
|
+
for i, tool in enumerate(AI_TOOLS, 1):
|
|
39
|
+
installed = is_installed(root, tool)
|
|
40
|
+
marker = f" {green}[installed]{reset}" if installed else ""
|
|
41
|
+
print(f" {i}. {tool['description']:<20s} {tool['context_file']}{marker}")
|
|
42
|
+
|
|
43
|
+
# tina4-ai tools option
|
|
44
|
+
tina4_ai_installed = shutil.which("mdview") is not None
|
|
45
|
+
marker = f" {green}[installed]{reset}" if tina4_ai_installed else ""
|
|
46
|
+
print(f" 8. Install tina4-ai tools (requires Python){marker}")
|
|
47
|
+
print()
|
|
48
|
+
return input(" Select (comma-separated, or 'all'): ").strip()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def install_selected(root: str, selection: str) -> list[str]:
|
|
52
|
+
"""Install context files for the selected tools.
|
|
53
|
+
|
|
54
|
+
selection: comma-separated numbers like "1,2,3" or "all"
|
|
55
|
+
Returns list of created/updated file paths.
|
|
56
|
+
"""
|
|
57
|
+
root_path = Path(root).resolve()
|
|
58
|
+
created = []
|
|
59
|
+
|
|
60
|
+
if selection.lower() == "all":
|
|
61
|
+
indices = list(range(len(AI_TOOLS)))
|
|
62
|
+
install_tina4_ai = True
|
|
63
|
+
else:
|
|
64
|
+
parts = [s.strip() for s in selection.split(",") if s.strip()]
|
|
65
|
+
indices = []
|
|
66
|
+
install_tina4_ai = False
|
|
67
|
+
for p in parts:
|
|
68
|
+
try:
|
|
69
|
+
n = int(p)
|
|
70
|
+
if n == 8:
|
|
71
|
+
install_tina4_ai = True
|
|
72
|
+
elif 1 <= n <= len(AI_TOOLS):
|
|
73
|
+
indices.append(n - 1)
|
|
74
|
+
except ValueError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
context = generate_context()
|
|
78
|
+
|
|
79
|
+
for idx in indices:
|
|
80
|
+
tool = AI_TOOLS[idx]
|
|
81
|
+
files = _install_for_tool(root_path, tool, context)
|
|
82
|
+
created.extend(files)
|
|
83
|
+
|
|
84
|
+
if install_tina4_ai:
|
|
85
|
+
_install_tina4_ai()
|
|
86
|
+
|
|
87
|
+
return created
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def install_all(root: str = ".") -> list[str]:
|
|
91
|
+
"""Install context for all AI tools (non-interactive)."""
|
|
92
|
+
return install_selected(root, "all")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
|
|
96
|
+
"""Install context file for a single tool."""
|
|
97
|
+
created = []
|
|
98
|
+
context_path = root / tool["context_file"]
|
|
99
|
+
|
|
100
|
+
# Create directories
|
|
101
|
+
if tool.get("config_dir"):
|
|
102
|
+
(root / tool["config_dir"]).mkdir(parents=True, exist_ok=True)
|
|
103
|
+
context_path.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
# Always overwrite — user chose to install
|
|
106
|
+
context_path.write_text(context, encoding="utf-8")
|
|
107
|
+
action = "Updated" if context_path.exists() else "Installed"
|
|
108
|
+
rel = str(context_path.relative_to(root))
|
|
109
|
+
created.append(rel)
|
|
110
|
+
print(f" \033[32m✓\033[0m {action} {rel}")
|
|
111
|
+
|
|
112
|
+
# Claude-specific extras
|
|
113
|
+
if tool["name"] == "claude-code":
|
|
114
|
+
skills = _install_claude_skills(root)
|
|
115
|
+
created.extend(skills)
|
|
116
|
+
|
|
117
|
+
return created
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _install_tina4_ai():
|
|
121
|
+
"""Install tina4-ai package (provides mdview for markdown viewing)."""
|
|
122
|
+
print(" Installing tina4-ai tools...")
|
|
123
|
+
for cmd in ["pip3", "pip"]:
|
|
124
|
+
if shutil.which(cmd):
|
|
125
|
+
try:
|
|
126
|
+
result = subprocess.run(
|
|
127
|
+
[cmd, "install", "--upgrade", "tina4-ai"],
|
|
128
|
+
capture_output=True, text=True, timeout=60,
|
|
129
|
+
)
|
|
130
|
+
if result.returncode == 0:
|
|
131
|
+
print(" \033[32m✓\033[0m Installed tina4-ai (mdview)")
|
|
132
|
+
return
|
|
133
|
+
else:
|
|
134
|
+
print(f" \033[33m!\033[0m {cmd} failed: {result.stderr.strip()[:100]}")
|
|
135
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
136
|
+
continue
|
|
137
|
+
print(" \033[33m!\033[0m Python/pip not available — skip tina4-ai")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _install_claude_skills(root: Path) -> list[str]:
|
|
141
|
+
"""Copy Claude Code skill files from the framework's templates."""
|
|
142
|
+
created = []
|
|
143
|
+
commands_dir = root / ".claude" / "commands"
|
|
144
|
+
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
pkg_dir = Path(__file__).parent.parent
|
|
147
|
+
source_dirs = [pkg_dir / "templates" / "ai" / "claude-commands"]
|
|
148
|
+
|
|
149
|
+
for source_dir in source_dirs:
|
|
150
|
+
if source_dir.is_dir():
|
|
151
|
+
for skill_file in source_dir.glob("*.md"):
|
|
152
|
+
target = commands_dir / skill_file.name
|
|
153
|
+
target.write_text(skill_file.read_text(encoding="utf-8"), encoding="utf-8")
|
|
154
|
+
rel = str(target.relative_to(root))
|
|
155
|
+
created.append(rel)
|
|
156
|
+
|
|
157
|
+
# Copy skill directories from framework .claude/skills/
|
|
158
|
+
framework_root = pkg_dir.parent
|
|
159
|
+
framework_skills_dir = framework_root / ".claude" / "skills"
|
|
160
|
+
if framework_skills_dir.is_dir():
|
|
161
|
+
target_skills_dir = root / ".claude" / "skills"
|
|
162
|
+
target_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
for skill_dir in framework_skills_dir.iterdir():
|
|
164
|
+
if skill_dir.is_dir():
|
|
165
|
+
target_dir = target_skills_dir / skill_dir.name
|
|
166
|
+
if target_dir.exists():
|
|
167
|
+
shutil.rmtree(target_dir)
|
|
168
|
+
shutil.copytree(skill_dir, target_dir)
|
|
169
|
+
rel = str(target_dir.relative_to(root))
|
|
170
|
+
created.append(rel)
|
|
171
|
+
print(f" \033[32m✓\033[0m Updated {rel}")
|
|
172
|
+
|
|
173
|
+
return created
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def generate_context() -> str:
|
|
177
|
+
"""Generate the universal Tina4 context document for any AI assistant."""
|
|
178
|
+
return f"""# Tina4 Python — AI Context
|
|
179
|
+
|
|
180
|
+
This project uses **Tina4 Python**, a lightweight, batteries-included web framework
|
|
181
|
+
with zero third-party dependencies for core features.
|
|
182
|
+
|
|
183
|
+
**Documentation:** https://tina4.com
|
|
184
|
+
|
|
185
|
+
## Quick Start
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
tina4python init . # Scaffold project
|
|
189
|
+
tina4python serve # Start dev server on port 7145
|
|
190
|
+
tina4python migrate # Run database migrations
|
|
191
|
+
tina4python test # Run test suite
|
|
192
|
+
tina4python routes # List all registered routes
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Project Structure
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
src/routes/ — Route handlers (auto-discovered, one per resource)
|
|
199
|
+
src/orm/ — ORM models (one per file, filename = class name)
|
|
200
|
+
src/templates/ — Twig/Jinja2 templates (extends base.twig)
|
|
201
|
+
src/app/ — Shared helpers and service classes
|
|
202
|
+
src/scss/ — SCSS files (auto-compiled to public/css/)
|
|
203
|
+
src/public/ — Static assets served at /
|
|
204
|
+
src/locales/ — Translation JSON files
|
|
205
|
+
src/seeds/ — Database seeder scripts
|
|
206
|
+
migrations/ — SQL migration files (sequential numbered)
|
|
207
|
+
tests/ — pytest test files
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Built-in Features (No External Packages Needed)
|
|
211
|
+
|
|
212
|
+
| Feature | Module | Import |
|
|
213
|
+
|---------|--------|--------|
|
|
214
|
+
| Routing | router | `from tina4_python.core.router import get, post, put, delete` |
|
|
215
|
+
| ORM | orm | `from tina4_python.orm import ORM, IntegerField, StringField` |
|
|
216
|
+
| Database | database | `from tina4_python.database import Database` |
|
|
217
|
+
| Templates | template | `response.render("page.twig", data)` |
|
|
218
|
+
| JWT Auth | auth | `from tina4_python.auth import Auth, hash_password, check_password` |
|
|
219
|
+
| REST API Client | api | `from tina4_python.api import Api` |
|
|
220
|
+
| GraphQL | graphql | `from tina4_python.graphql import GraphQL, Schema` |
|
|
221
|
+
| WebSocket | websocket | `from tina4_python.websocket import WebSocketServer` |
|
|
222
|
+
| SOAP/WSDL | wsdl | `from tina4_python.wsdl import WSDL, wsdl_operation` |
|
|
223
|
+
| Email (SMTP+IMAP) | messenger | `from tina4_python.messenger import Messenger` |
|
|
224
|
+
| Background Queue | queue | `from tina4_python.queue import Queue` |
|
|
225
|
+
| SCSS Compilation | scss | Auto-compiled from src/scss/ |
|
|
226
|
+
| Migrations | migration | `tina4python migrate` CLI command |
|
|
227
|
+
| Seeder | seeder | `from tina4_python.seeder import FakeData, seed_table` |
|
|
228
|
+
| i18n | localization | `from tina4_python.localization import Localization` |
|
|
229
|
+
| Swagger/OpenAPI | swagger | Auto-generated at /swagger |
|
|
230
|
+
| Sessions | session | `request.session.get(key)` / `.set(key, value)` |
|
|
231
|
+
| Middleware | middleware | `@middleware(MyMiddleware)` decorator |
|
|
232
|
+
| HTML Builder | html_element | `from tina4_python.html_element import HTMLElement` |
|
|
233
|
+
| Form Tokens | template | `{{{{ form_token() }}}}` in Twig |
|
|
234
|
+
|
|
235
|
+
## Key Conventions
|
|
236
|
+
|
|
237
|
+
1. **Routes return `response()`** — always use `response(data)` not `response.json()`
|
|
238
|
+
2. **GET routes are public**, POST/PUT/PATCH/DELETE require auth by default
|
|
239
|
+
3. **Use `@noauth()`** to make write routes public, `@secured()` to protect GET routes
|
|
240
|
+
4. **Decorator order**: `@noauth/@secured` → `@description/@tags` → `@get/@post` (route decorator innermost)
|
|
241
|
+
5. **Every template extends `base.twig`** — no standalone HTML pages
|
|
242
|
+
6. **No inline styles** — use SCSS in `src/scss/` with CSS variables
|
|
243
|
+
7. **No hardcoded colors** — use `var(--primary)`, `var(--text)`, etc.
|
|
244
|
+
8. **All schema changes via migrations** — never create tables in route code
|
|
245
|
+
9. **Service pattern** — complex logic goes in `src/app/` service classes, routes stay thin
|
|
246
|
+
10. **Use built-in features** — never install packages for things Tina4 already provides
|
|
247
|
+
|
|
248
|
+
## AI Workflow — Available Skills
|
|
249
|
+
|
|
250
|
+
When using an AI coding assistant with Tina4, these skills are available:
|
|
251
|
+
|
|
252
|
+
| Skill | Description |
|
|
253
|
+
|-------|-------------|
|
|
254
|
+
| `/tina4-route` | Create a new route with proper decorators and auth |
|
|
255
|
+
| `/tina4-orm` | Create an ORM model with migration |
|
|
256
|
+
| `/tina4-crud` | Generate complete CRUD (migration, ORM, routes, template, tests) |
|
|
257
|
+
| `/tina4-auth` | Set up JWT authentication with login/register |
|
|
258
|
+
| `/tina4-api` | Create an external API integration |
|
|
259
|
+
| `/tina4-queue` | Set up background job processing |
|
|
260
|
+
| `/tina4-template` | Create a server-rendered template page |
|
|
261
|
+
| `/tina4-graphql` | Set up a GraphQL endpoint |
|
|
262
|
+
| `/tina4-websocket` | Set up WebSocket communication |
|
|
263
|
+
| `/tina4-wsdl` | Create a SOAP/WSDL service |
|
|
264
|
+
| `/tina4-messenger` | Set up email send/receive |
|
|
265
|
+
| `/tina4-test` | Write tests for a feature |
|
|
266
|
+
| `/tina4-migration` | Create a database migration |
|
|
267
|
+
| `/tina4-seed` | Generate fake data for development |
|
|
268
|
+
| `/tina4-i18n` | Set up internationalization |
|
|
269
|
+
| `/tina4-scss` | Set up SCSS stylesheets |
|
|
270
|
+
| `/tina4-frontend` | Set up a frontend framework |
|
|
271
|
+
|
|
272
|
+
## Common Patterns
|
|
273
|
+
|
|
274
|
+
### Route
|
|
275
|
+
```python
|
|
276
|
+
from tina4_python.core.router import get, post, noauth
|
|
277
|
+
from tina4_python.swagger import description, tags
|
|
278
|
+
|
|
279
|
+
@noauth()
|
|
280
|
+
@description("Create a widget")
|
|
281
|
+
@tags(["widgets"])
|
|
282
|
+
@post("/api/widgets")
|
|
283
|
+
async def create_widget(request, response):
|
|
284
|
+
data = request.body
|
|
285
|
+
return response({{"created": True}}, 201)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### ORM Model
|
|
289
|
+
```python
|
|
290
|
+
from tina4_python.orm import ORM, IntegerField, StringField
|
|
291
|
+
|
|
292
|
+
class Widget(ORM):
|
|
293
|
+
id = IntegerField(primary_key=True, auto_increment=True)
|
|
294
|
+
name = StringField()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Template
|
|
298
|
+
```twig
|
|
299
|
+
{{% extends "base.twig" %}}
|
|
300
|
+
{{% block content %}}
|
|
301
|
+
<div class="container">
|
|
302
|
+
<h1>{{{{ title }}}}</h1>
|
|
303
|
+
{{% for item in items %}}
|
|
304
|
+
<p>{{{{ item.name }}}}</p>
|
|
305
|
+
{{% endfor %}}
|
|
306
|
+
</div>
|
|
307
|
+
{{% endblock %}}
|
|
308
|
+
```
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = ["AI_TOOLS", "is_installed", "show_menu", "install_selected", "install_all", "generate_context"]
|
|
@@ -14,7 +14,7 @@ No PyJWT, no cryptography package.
|
|
|
14
14
|
payload = auth.valid_token(token)
|
|
15
15
|
|
|
16
16
|
hashed = Auth.hash_password("secret123")
|
|
17
|
-
Auth.check_password(
|
|
17
|
+
Auth.check_password("secret123", hashed) # True
|
|
18
18
|
"""
|
|
19
19
|
import os
|
|
20
20
|
import hmac
|
|
@@ -152,6 +152,25 @@ class Auth:
|
|
|
152
152
|
auth = cls(secret=secret, expires_in=expires_in)
|
|
153
153
|
return auth.refresh_token(token)
|
|
154
154
|
|
|
155
|
+
@classmethod
|
|
156
|
+
def authenticate_request_static(cls, headers: dict) -> dict | None:
|
|
157
|
+
"""Extract and validate auth from request headers without instantiating Auth.
|
|
158
|
+
|
|
159
|
+
Reads SECRET from env. Checks: Bearer JWT, Bearer API key, Basic auth.
|
|
160
|
+
Returns payload dict on success, None on failure.
|
|
161
|
+
"""
|
|
162
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
163
|
+
auth = cls(secret=secret)
|
|
164
|
+
return auth.authenticate_request(headers)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def validate_api_key_static(provided: str, expected: str = None) -> bool:
|
|
168
|
+
"""Validate an API key without instantiating Auth.
|
|
169
|
+
|
|
170
|
+
Alias for validate_api_key (already a staticmethod).
|
|
171
|
+
"""
|
|
172
|
+
return Auth.validate_api_key(provided, expected)
|
|
173
|
+
|
|
155
174
|
# ── Password Hashing ──────────────────────────────────────────
|
|
156
175
|
|
|
157
176
|
@staticmethod
|
|
@@ -164,7 +183,7 @@ class Auth:
|
|
|
164
183
|
return f"pbkdf2_sha256${iterations}${salt}${dk.hex()}"
|
|
165
184
|
|
|
166
185
|
@staticmethod
|
|
167
|
-
def check_password(
|
|
186
|
+
def check_password(password: str, hashed: str) -> bool:
|
|
168
187
|
"""Verify a password against its PBKDF2 hash."""
|
|
169
188
|
try:
|
|
170
189
|
parts = hashed.split("$")
|
|
@@ -183,9 +202,18 @@ class Auth:
|
|
|
183
202
|
# ── API Key Auth ──────────────────────────────────────────────
|
|
184
203
|
|
|
185
204
|
@staticmethod
|
|
186
|
-
def validate_api_key(provided: str) -> bool:
|
|
187
|
-
"""Check
|
|
188
|
-
|
|
205
|
+
def validate_api_key(provided: str, expected: str = None) -> bool:
|
|
206
|
+
"""Check an API key against an expected value.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
provided: The API key provided in the request.
|
|
210
|
+
expected: The expected API key. If None, reads from
|
|
211
|
+
TINA4_API_KEY env var (falls back to API_KEY).
|
|
212
|
+
|
|
213
|
+
Returns: True if the provided key matches.
|
|
214
|
+
"""
|
|
215
|
+
if expected is None:
|
|
216
|
+
expected = os.environ.get("TINA4_API_KEY", os.environ.get("API_KEY", ""))
|
|
189
217
|
if not expected:
|
|
190
218
|
return False
|
|
191
219
|
return hmac.compare_digest(provided, expected)
|
|
@@ -255,4 +283,17 @@ def refresh_token(token: str, expires_in: int = 60) -> str | None:
|
|
|
255
283
|
return Auth.refresh_token_static(token, expires_in=expires_in)
|
|
256
284
|
|
|
257
285
|
|
|
258
|
-
|
|
286
|
+
def authenticate_request(headers: dict) -> dict | None:
|
|
287
|
+
"""Validate auth from request headers — reads SECRET from env."""
|
|
288
|
+
return Auth.authenticate_request_static(headers)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def validate_api_key(provided: str, expected: str = None) -> bool:
|
|
292
|
+
"""Validate an API key. Shortcut for Auth.validate_api_key()."""
|
|
293
|
+
return Auth.validate_api_key(provided, expected)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
__all__ = [
|
|
297
|
+
"Auth", "get_token", "valid_token", "get_payload", "refresh_token",
|
|
298
|
+
"authenticate_request", "validate_api_key",
|
|
299
|
+
]
|
|
@@ -363,7 +363,7 @@ def _routes(args):
|
|
|
363
363
|
importlib.import_module("app")
|
|
364
364
|
|
|
365
365
|
from tina4_python.core.router import Router
|
|
366
|
-
routes = Router.
|
|
366
|
+
routes = Router.get_routes()
|
|
367
367
|
if not routes:
|
|
368
368
|
print("No routes registered.")
|
|
369
369
|
return
|
|
@@ -405,42 +405,19 @@ def _build(args):
|
|
|
405
405
|
|
|
406
406
|
|
|
407
407
|
def _ai(args):
|
|
408
|
-
"""
|
|
409
|
-
from tina4_python.ai import
|
|
408
|
+
"""Install AI coding assistant context files."""
|
|
409
|
+
from tina4_python.ai import show_menu, install_selected, install_all
|
|
410
410
|
|
|
411
411
|
root = "."
|
|
412
412
|
|
|
413
|
-
if "
|
|
414
|
-
#
|
|
415
|
-
|
|
416
|
-
if created:
|
|
417
|
-
print("Installed Tina4 context for all AI tools:")
|
|
418
|
-
for f in created:
|
|
419
|
-
print(f" + {f}")
|
|
420
|
-
else:
|
|
421
|
-
print("All AI context files already exist. Use --force to overwrite.")
|
|
422
|
-
elif "--status" in args or not args:
|
|
423
|
-
# Show detection status
|
|
424
|
-
print(status_report(root))
|
|
425
|
-
|
|
426
|
-
# Auto-install for detected tools
|
|
427
|
-
detected = [t for t in detect_ai(root) if t["installed"]]
|
|
428
|
-
if detected:
|
|
429
|
-
created = install_context(root)
|
|
430
|
-
if created:
|
|
431
|
-
print("Installed Tina4 context:")
|
|
432
|
-
for f in created:
|
|
433
|
-
print(f" + {f}")
|
|
434
|
-
else:
|
|
435
|
-
print("Context files already exist. Use --force to overwrite.")
|
|
413
|
+
if args and args[0].lower() == "all":
|
|
414
|
+
# Non-interactive: install everything
|
|
415
|
+
install_all(root)
|
|
436
416
|
else:
|
|
437
|
-
#
|
|
438
|
-
|
|
439
|
-
if
|
|
440
|
-
|
|
441
|
-
print(f" + {f}")
|
|
442
|
-
else:
|
|
443
|
-
print("Nothing to install.")
|
|
417
|
+
# Interactive: show menu, get selection
|
|
418
|
+
selection = show_menu(root)
|
|
419
|
+
if selection:
|
|
420
|
+
install_selected(root, selection)
|
|
444
421
|
|
|
445
422
|
|
|
446
423
|
def _generate(args):
|
|
@@ -231,8 +231,13 @@ class Router:
|
|
|
231
231
|
return None, {}
|
|
232
232
|
|
|
233
233
|
@staticmethod
|
|
234
|
-
def
|
|
235
|
-
"""Return all registered routes
|
|
234
|
+
def get_routes() -> list[dict]:
|
|
235
|
+
"""Return all registered routes."""
|
|
236
|
+
return _routes
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def list_routes() -> list[dict]:
|
|
240
|
+
"""Return all registered routes (debug-friendly)."""
|
|
236
241
|
return _routes
|
|
237
242
|
|
|
238
243
|
@staticmethod
|
|
@@ -757,7 +757,7 @@ async def app(scope: dict, receive, send):
|
|
|
757
757
|
# Serve OpenAPI spec JSON from all registered routes
|
|
758
758
|
from tina4_python.swagger import Swagger as _SwaggerGen
|
|
759
759
|
_swagger = _SwaggerGen()
|
|
760
|
-
_spec = _swagger.generate(Router.
|
|
760
|
+
_spec = _swagger.generate(Router.get_routes())
|
|
761
761
|
response.json(_spec)
|
|
762
762
|
_cors.apply(request, response)
|
|
763
763
|
headers = response.build_headers("")
|
|
@@ -929,7 +929,7 @@ async def app(scope: dict, receive, send):
|
|
|
929
929
|
matched_pattern = route["path"] if route else "-"
|
|
930
930
|
toolbar = render_dev_toolbar(
|
|
931
931
|
request.method, request.path, matched_pattern,
|
|
932
|
-
request_id, len(Router.
|
|
932
|
+
request_id, len(Router.get_routes()),
|
|
933
933
|
).encode()
|
|
934
934
|
content = response.content
|
|
935
935
|
# Inject before </body> if present, else append
|
|
@@ -1216,7 +1216,7 @@ def run(host: str | None = None, port: int | None = None):
|
|
|
1216
1216
|
|
|
1217
1217
|
# Auto-discover routes
|
|
1218
1218
|
_auto_discover("src")
|
|
1219
|
-
route_count = len(Router.
|
|
1219
|
+
route_count = len(Router.get_routes())
|
|
1220
1220
|
Log.info(f"Discovered {route_count} routes")
|
|
1221
1221
|
|
|
1222
1222
|
# Resolve host/port (CLI arg > ENV > default)
|