tina4-python 3.11.13__tar.gz → 3.11.14__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.11.13 → tina4_python-3.11.14}/PKG-INFO +1 -1
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/__init__.py +1 -1
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/firebird.py +10 -2
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/postgres.py +10 -2
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/dev_admin/__init__.py +557 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/.gitignore +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/README.md +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/pyproject.toml +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/Testing.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/events.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/request.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/response.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/router.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/core/server.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -158,7 +158,7 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
158
158
|
|
|
159
159
|
desc = cursor.description
|
|
160
160
|
col_names = [d[0].strip().lower() for d in desc] if desc else []
|
|
161
|
-
rows = [dict(zip(col_names, row)) for row in cursor.fetchall()]
|
|
161
|
+
rows = [self._decode_blobs(dict(zip(col_names, row))) for row in cursor.fetchall()]
|
|
162
162
|
|
|
163
163
|
return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
|
|
164
164
|
|
|
@@ -171,7 +171,15 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
171
171
|
if row is None:
|
|
172
172
|
return None
|
|
173
173
|
col_names = [d[0].strip().lower() for d in desc] if desc else []
|
|
174
|
-
return dict(zip(col_names, row))
|
|
174
|
+
return self._decode_blobs(dict(zip(col_names, row)))
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _decode_blobs(row: dict) -> dict:
|
|
178
|
+
"""Ensure Firebird BLOB columns are proper bytes, not memoryview."""
|
|
179
|
+
for key, value in row.items():
|
|
180
|
+
if isinstance(value, memoryview):
|
|
181
|
+
row[key] = bytes(value)
|
|
182
|
+
return row
|
|
175
183
|
|
|
176
184
|
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
177
185
|
columns = ", ".join(data.keys())
|
|
@@ -119,7 +119,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
119
119
|
paginated_sql = f"{sql} LIMIT %s OFFSET %s"
|
|
120
120
|
paginated_params = (params or []) + [limit, offset]
|
|
121
121
|
cursor.execute(paginated_sql, paginated_params)
|
|
122
|
-
rows = [dict(row) for row in cursor.fetchall()]
|
|
122
|
+
rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
|
|
123
123
|
|
|
124
124
|
return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
|
|
125
125
|
|
|
@@ -130,7 +130,15 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
130
130
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
131
131
|
cursor.execute(sql, params or [])
|
|
132
132
|
row = cursor.fetchone()
|
|
133
|
-
return dict(row) if row else None
|
|
133
|
+
return self._decode_blobs(dict(row)) if row else None
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _decode_blobs(row: dict) -> dict:
|
|
137
|
+
"""Ensure binary columns (bytea) are proper bytes, not memoryview."""
|
|
138
|
+
for key, value in row.items():
|
|
139
|
+
if isinstance(value, memoryview):
|
|
140
|
+
row[key] = bytes(value)
|
|
141
|
+
return row
|
|
134
142
|
|
|
135
143
|
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
136
144
|
columns = ", ".join(data.keys())
|
|
@@ -338,6 +338,16 @@ def get_api_handlers() -> dict:
|
|
|
338
338
|
"/__dev/api/metrics/full": ("GET", _api_metrics_full),
|
|
339
339
|
"/__dev/api/metrics/file": ("GET", _api_metrics_file),
|
|
340
340
|
"/__dev/api/graphql/schema": ("GET", _api_graphql_schema),
|
|
341
|
+
# ── Editor endpoints ──
|
|
342
|
+
"/__dev/api/files": ("GET", _api_files),
|
|
343
|
+
"/__dev/api/file": ("GET", _api_file_read),
|
|
344
|
+
"/__dev/api/file/save": ("POST", _api_file_save),
|
|
345
|
+
"/__dev/api/file/raw": ("GET", _api_file_raw),
|
|
346
|
+
"/__dev/api/file/rename": ("POST", _api_file_rename),
|
|
347
|
+
"/__dev/api/file/delete": ("POST", _api_file_delete),
|
|
348
|
+
"/__dev/api/deps/search": ("GET", _api_deps_search),
|
|
349
|
+
"/__dev/api/deps/install": ("POST", _api_deps_install),
|
|
350
|
+
"/__dev/api/git/status": ("GET", _api_git_status),
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
|
|
@@ -1499,5 +1509,552 @@ function tina4VersionModal(){{
|
|
|
1499
1509
|
</script>"""
|
|
1500
1510
|
|
|
1501
1511
|
|
|
1512
|
+
# ── Editor API endpoints ──────────────────────────────────────
|
|
1513
|
+
|
|
1514
|
+
async def _api_files(request, response):
|
|
1515
|
+
"""List files in a directory with git status.
|
|
1516
|
+
|
|
1517
|
+
Query params:
|
|
1518
|
+
path — relative directory path (default: project root)
|
|
1519
|
+
"""
|
|
1520
|
+
import os, subprocess
|
|
1521
|
+
rel = (request.params.get("path") or "").strip("/")
|
|
1522
|
+
base = os.getcwd()
|
|
1523
|
+
target = os.path.normpath(os.path.join(base, rel))
|
|
1524
|
+
|
|
1525
|
+
# Security: must stay within project root
|
|
1526
|
+
if not target.startswith(base):
|
|
1527
|
+
return response({"error": "Path outside project"}, 403)
|
|
1528
|
+
|
|
1529
|
+
if not os.path.isdir(target):
|
|
1530
|
+
return response({"error": "Not a directory"}, 404)
|
|
1531
|
+
|
|
1532
|
+
# Find git root (may differ from cwd)
|
|
1533
|
+
git_root = base
|
|
1534
|
+
try:
|
|
1535
|
+
out = subprocess.run(
|
|
1536
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
1537
|
+
capture_output=True, text=True, cwd=base, timeout=5
|
|
1538
|
+
)
|
|
1539
|
+
if out.returncode == 0:
|
|
1540
|
+
git_root = out.stdout.strip().replace("\\", "/")
|
|
1541
|
+
except Exception:
|
|
1542
|
+
pass
|
|
1543
|
+
|
|
1544
|
+
# Prefix to prepend to entry_rel paths to match git's paths
|
|
1545
|
+
cwd_in_git = os.path.relpath(base, git_root).replace("\\", "/")
|
|
1546
|
+
if cwd_in_git == ".":
|
|
1547
|
+
cwd_in_git = ""
|
|
1548
|
+
else:
|
|
1549
|
+
cwd_in_git += "/"
|
|
1550
|
+
|
|
1551
|
+
# Get git status for the project
|
|
1552
|
+
git_status = {}
|
|
1553
|
+
try:
|
|
1554
|
+
out = subprocess.run(
|
|
1555
|
+
["git", "status", "--porcelain", "-uall"],
|
|
1556
|
+
capture_output=True, text=True, cwd=base, timeout=5
|
|
1557
|
+
)
|
|
1558
|
+
for line in out.stdout.strip().split("\n"):
|
|
1559
|
+
if len(line) >= 4:
|
|
1560
|
+
status_code = line[:2].strip()
|
|
1561
|
+
file_path = line[3:].strip()
|
|
1562
|
+
git_status[file_path] = status_code
|
|
1563
|
+
except Exception:
|
|
1564
|
+
pass
|
|
1565
|
+
|
|
1566
|
+
# Get current branch
|
|
1567
|
+
branch = ""
|
|
1568
|
+
try:
|
|
1569
|
+
out = subprocess.run(
|
|
1570
|
+
["git", "branch", "--show-current"],
|
|
1571
|
+
capture_output=True, text=True, cwd=base, timeout=5
|
|
1572
|
+
)
|
|
1573
|
+
branch = out.stdout.strip()
|
|
1574
|
+
except Exception:
|
|
1575
|
+
pass
|
|
1576
|
+
|
|
1577
|
+
entries = []
|
|
1578
|
+
try:
|
|
1579
|
+
for name in sorted(os.listdir(target)):
|
|
1580
|
+
full = os.path.join(target, name)
|
|
1581
|
+
entry_rel = os.path.relpath(full, base).replace("\\", "/")
|
|
1582
|
+
|
|
1583
|
+
# Skip hidden dirs and noise
|
|
1584
|
+
if name.startswith(".") and name not in (".env", ".env.example"):
|
|
1585
|
+
continue
|
|
1586
|
+
if name in ("__pycache__", "node_modules", "vendor", ".git",
|
|
1587
|
+
"venv", ".venv", "dist", "target", ".tina4"):
|
|
1588
|
+
continue
|
|
1589
|
+
|
|
1590
|
+
is_dir = os.path.isdir(full)
|
|
1591
|
+
|
|
1592
|
+
# Git path = cwd_in_git + entry_rel (what git reports)
|
|
1593
|
+
git_path = cwd_in_git + entry_rel
|
|
1594
|
+
|
|
1595
|
+
# Git status for this entry
|
|
1596
|
+
status = "clean"
|
|
1597
|
+
if git_path in git_status:
|
|
1598
|
+
code = git_status[git_path]
|
|
1599
|
+
if code == "??":
|
|
1600
|
+
status = "untracked"
|
|
1601
|
+
elif "M" in code:
|
|
1602
|
+
status = "modified"
|
|
1603
|
+
elif "A" in code:
|
|
1604
|
+
status = "added"
|
|
1605
|
+
elif "D" in code:
|
|
1606
|
+
status = "deleted"
|
|
1607
|
+
elif is_dir:
|
|
1608
|
+
# Check if any child is dirty
|
|
1609
|
+
dir_prefix = git_path + "/"
|
|
1610
|
+
for gf, gc in git_status.items():
|
|
1611
|
+
if gf.startswith(dir_prefix):
|
|
1612
|
+
if gc == "??":
|
|
1613
|
+
status = "untracked"
|
|
1614
|
+
else:
|
|
1615
|
+
status = "modified"
|
|
1616
|
+
break
|
|
1617
|
+
|
|
1618
|
+
# Check if directory has children (for arrow display)
|
|
1619
|
+
has_children = False
|
|
1620
|
+
if is_dir:
|
|
1621
|
+
try:
|
|
1622
|
+
contents = os.listdir(full)
|
|
1623
|
+
has_children = any(
|
|
1624
|
+
not n.startswith(".") and n not in (
|
|
1625
|
+
"__pycache__", "node_modules", "vendor",
|
|
1626
|
+
".git", "venv", ".venv", "dist", "target", ".tina4"
|
|
1627
|
+
) for n in contents
|
|
1628
|
+
)
|
|
1629
|
+
except PermissionError:
|
|
1630
|
+
pass
|
|
1631
|
+
|
|
1632
|
+
entries.append({
|
|
1633
|
+
"name": name,
|
|
1634
|
+
"path": entry_rel,
|
|
1635
|
+
"is_dir": is_dir,
|
|
1636
|
+
"has_children": has_children if is_dir else None,
|
|
1637
|
+
"git_status": status,
|
|
1638
|
+
"size": os.path.getsize(full) if not is_dir else None,
|
|
1639
|
+
})
|
|
1640
|
+
except PermissionError:
|
|
1641
|
+
return response({"error": "Permission denied"}, 403)
|
|
1642
|
+
|
|
1643
|
+
return response({
|
|
1644
|
+
"path": rel or ".",
|
|
1645
|
+
"branch": branch,
|
|
1646
|
+
"entries": entries,
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
async def _api_file_read(request, response):
|
|
1651
|
+
"""Read a file's content.
|
|
1652
|
+
|
|
1653
|
+
Query params:
|
|
1654
|
+
path — relative file path
|
|
1655
|
+
"""
|
|
1656
|
+
import os
|
|
1657
|
+
rel = (request.params.get("path") or "").strip("/")
|
|
1658
|
+
if not rel:
|
|
1659
|
+
return response({"error": "path required"}, 400)
|
|
1660
|
+
|
|
1661
|
+
base = os.getcwd()
|
|
1662
|
+
target = os.path.normpath(os.path.join(base, rel))
|
|
1663
|
+
|
|
1664
|
+
if not target.startswith(base):
|
|
1665
|
+
return response({"error": "Path outside project"}, 403)
|
|
1666
|
+
|
|
1667
|
+
if not os.path.isfile(target):
|
|
1668
|
+
return response({"error": "File not found"}, 404)
|
|
1669
|
+
|
|
1670
|
+
# Size guard: don't load huge files into JSON
|
|
1671
|
+
size = os.path.getsize(target)
|
|
1672
|
+
if size > 2 * 1024 * 1024: # 2MB
|
|
1673
|
+
return response({"error": "File too large", "size": size}, 413)
|
|
1674
|
+
|
|
1675
|
+
try:
|
|
1676
|
+
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
|
1677
|
+
content = f.read()
|
|
1678
|
+
except Exception as e:
|
|
1679
|
+
return response({"error": str(e)}, 500)
|
|
1680
|
+
|
|
1681
|
+
# Detect language from extension
|
|
1682
|
+
ext = os.path.splitext(rel)[1].lower()
|
|
1683
|
+
# Also detect Dockerfile (no extension)
|
|
1684
|
+
basename = os.path.basename(rel)
|
|
1685
|
+
if basename.lower() in ("dockerfile", "dockerfile.dev", "dockerfile.prod"):
|
|
1686
|
+
return response({
|
|
1687
|
+
"path": rel, "content": content,
|
|
1688
|
+
"language": "dockerfile", "size": size,
|
|
1689
|
+
})
|
|
1690
|
+
|
|
1691
|
+
lang_map = {
|
|
1692
|
+
".py": "python", ".php": "php", ".rb": "ruby",
|
|
1693
|
+
".ts": "typescript", ".js": "javascript", ".jsx": "javascript",
|
|
1694
|
+
".tsx": "typescript", ".json": "json", ".html": "html",
|
|
1695
|
+
".twig": "html", ".css": "css", ".scss": "css",
|
|
1696
|
+
".md": "markdown", ".sql": "sql", ".yaml": "yaml",
|
|
1697
|
+
".yml": "yaml", ".toml": "toml", ".xml": "html",
|
|
1698
|
+
".env": "env", ".env.example": "env",
|
|
1699
|
+
".sh": "shell", ".bash": "shell",
|
|
1700
|
+
".bat": "shell", ".cmd": "shell", ".ps1": "shell",
|
|
1701
|
+
".rs": "rust", ".go": "go", ".java": "java",
|
|
1702
|
+
".txt": "text", ".csv": "text", ".log": "text",
|
|
1703
|
+
".gemspec": "ruby", ".rake": "ruby",
|
|
1704
|
+
".svg": "svg",
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return response({
|
|
1708
|
+
"path": rel,
|
|
1709
|
+
"content": content,
|
|
1710
|
+
"language": lang_map.get(ext, "text"),
|
|
1711
|
+
"size": size,
|
|
1712
|
+
})
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
async def _api_file_raw(request, response):
|
|
1716
|
+
"""Serve a raw file with correct content-type (for image preview etc).
|
|
1717
|
+
|
|
1718
|
+
Query params:
|
|
1719
|
+
path — relative file path
|
|
1720
|
+
"""
|
|
1721
|
+
import os, mimetypes
|
|
1722
|
+
rel = (request.params.get("path") or "").strip("/")
|
|
1723
|
+
if not rel:
|
|
1724
|
+
return response({"error": "path required"}, 400)
|
|
1725
|
+
|
|
1726
|
+
base = os.getcwd()
|
|
1727
|
+
target = os.path.normpath(os.path.join(base, rel))
|
|
1728
|
+
|
|
1729
|
+
if not target.startswith(base):
|
|
1730
|
+
return response({"error": "Path outside project"}, 403)
|
|
1731
|
+
if not os.path.isfile(target):
|
|
1732
|
+
return response({"error": "File not found"}, 404)
|
|
1733
|
+
|
|
1734
|
+
# Size guard
|
|
1735
|
+
size = os.path.getsize(target)
|
|
1736
|
+
if size > 10 * 1024 * 1024:
|
|
1737
|
+
return response({"error": "File too large"}, 413)
|
|
1738
|
+
|
|
1739
|
+
content_type = mimetypes.guess_type(target)[0] or "application/octet-stream"
|
|
1740
|
+
|
|
1741
|
+
try:
|
|
1742
|
+
with open(target, "rb") as f:
|
|
1743
|
+
data = f.read()
|
|
1744
|
+
except Exception as e:
|
|
1745
|
+
return response({"error": str(e)}, 500)
|
|
1746
|
+
|
|
1747
|
+
from tina4_python.core.response import Response
|
|
1748
|
+
Response.add_header("Content-Type", content_type)
|
|
1749
|
+
Response.add_header("Cache-Control", "no-cache")
|
|
1750
|
+
# Return raw bytes — the response handler will detect binary
|
|
1751
|
+
import base64
|
|
1752
|
+
return response({
|
|
1753
|
+
"_raw": True,
|
|
1754
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
1755
|
+
"content_type": content_type,
|
|
1756
|
+
"size": size,
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
async def _api_file_save(request, response):
|
|
1761
|
+
"""Save content to a file.
|
|
1762
|
+
|
|
1763
|
+
Body: { "path": "...", "content": "..." }
|
|
1764
|
+
"""
|
|
1765
|
+
import os
|
|
1766
|
+
body = request.body or {}
|
|
1767
|
+
rel = (body.get("path") or "").strip("/")
|
|
1768
|
+
content = body.get("content")
|
|
1769
|
+
|
|
1770
|
+
if not rel:
|
|
1771
|
+
return response({"error": "path required"}, 400)
|
|
1772
|
+
if content is None:
|
|
1773
|
+
return response({"error": "content required"}, 400)
|
|
1774
|
+
|
|
1775
|
+
base = os.getcwd()
|
|
1776
|
+
target = os.path.normpath(os.path.join(base, rel))
|
|
1777
|
+
|
|
1778
|
+
if not target.startswith(base):
|
|
1779
|
+
return response({"error": "Path outside project"}, 403)
|
|
1780
|
+
|
|
1781
|
+
# Don't allow overwriting framework internals
|
|
1782
|
+
if "tina4_python/" in rel or "vendor/" in rel or "node_modules/" in rel:
|
|
1783
|
+
return response({"error": "Cannot write to framework directories"}, 403)
|
|
1784
|
+
|
|
1785
|
+
try:
|
|
1786
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
1787
|
+
with open(target, "w", encoding="utf-8", newline="") as f:
|
|
1788
|
+
f.write(content)
|
|
1789
|
+
except Exception as e:
|
|
1790
|
+
return response({"error": str(e)}, 500)
|
|
1791
|
+
|
|
1792
|
+
return response({"saved": True, "path": rel, "size": len(content)})
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
async def _api_file_rename(request, response):
|
|
1796
|
+
"""Rename/move a file or directory.
|
|
1797
|
+
|
|
1798
|
+
Body: { "from": "old/path", "to": "new/path" }
|
|
1799
|
+
"""
|
|
1800
|
+
import os, shutil
|
|
1801
|
+
body = request.body or {}
|
|
1802
|
+
from_rel = (body.get("from") or "").strip("/")
|
|
1803
|
+
to_rel = (body.get("to") or "").strip("/")
|
|
1804
|
+
if not from_rel or not to_rel:
|
|
1805
|
+
return response({"error": "from and to required"}, 400)
|
|
1806
|
+
|
|
1807
|
+
base = os.getcwd()
|
|
1808
|
+
from_abs = os.path.normpath(os.path.join(base, from_rel))
|
|
1809
|
+
to_abs = os.path.normpath(os.path.join(base, to_rel))
|
|
1810
|
+
|
|
1811
|
+
if not from_abs.startswith(base) or not to_abs.startswith(base):
|
|
1812
|
+
return response({"error": "Path outside project"}, 403)
|
|
1813
|
+
if not os.path.exists(from_abs):
|
|
1814
|
+
return response({"error": "Source not found"}, 404)
|
|
1815
|
+
|
|
1816
|
+
try:
|
|
1817
|
+
os.makedirs(os.path.dirname(to_abs), exist_ok=True)
|
|
1818
|
+
shutil.move(from_abs, to_abs)
|
|
1819
|
+
except Exception as e:
|
|
1820
|
+
return response({"error": str(e)}, 500)
|
|
1821
|
+
|
|
1822
|
+
return response({"renamed": True, "from": from_rel, "to": to_rel})
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
async def _api_file_delete(request, response):
|
|
1826
|
+
"""Delete a file or directory.
|
|
1827
|
+
|
|
1828
|
+
Body: { "path": "...", "is_dir": false }
|
|
1829
|
+
"""
|
|
1830
|
+
import os, shutil
|
|
1831
|
+
body = request.body or {}
|
|
1832
|
+
rel = (body.get("path") or "").strip("/")
|
|
1833
|
+
is_dir = body.get("is_dir", False)
|
|
1834
|
+
|
|
1835
|
+
if not rel:
|
|
1836
|
+
return response({"error": "path required"}, 400)
|
|
1837
|
+
|
|
1838
|
+
base = os.getcwd()
|
|
1839
|
+
target = os.path.normpath(os.path.join(base, rel))
|
|
1840
|
+
|
|
1841
|
+
if not target.startswith(base):
|
|
1842
|
+
return response({"error": "Path outside project"}, 403)
|
|
1843
|
+
if not os.path.exists(target):
|
|
1844
|
+
return response({"error": "Not found"}, 404)
|
|
1845
|
+
|
|
1846
|
+
# Safety: don't delete project root or key config
|
|
1847
|
+
if rel in (".", "..", "app.py", "index.php", "app.rb", "app.ts",
|
|
1848
|
+
"composer.json", "Gemfile", "package.json", "pyproject.toml"):
|
|
1849
|
+
return response({"error": "Cannot delete project root files"}, 403)
|
|
1850
|
+
|
|
1851
|
+
try:
|
|
1852
|
+
if is_dir:
|
|
1853
|
+
shutil.rmtree(target)
|
|
1854
|
+
else:
|
|
1855
|
+
os.remove(target)
|
|
1856
|
+
except Exception as e:
|
|
1857
|
+
return response({"error": str(e)}, 500)
|
|
1858
|
+
|
|
1859
|
+
return response({"deleted": True, "path": rel})
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
async def _api_deps_search(request, response):
|
|
1863
|
+
"""Search package registries.
|
|
1864
|
+
|
|
1865
|
+
Query params: q (search term), registry (pypi|npm|packagist|rubygems|crates)
|
|
1866
|
+
"""
|
|
1867
|
+
import urllib.request, json
|
|
1868
|
+
query = request.params.get("q", "").strip()
|
|
1869
|
+
registry = request.params.get("registry", "pypi")
|
|
1870
|
+
if not query:
|
|
1871
|
+
return response({"packages": []})
|
|
1872
|
+
|
|
1873
|
+
packages = []
|
|
1874
|
+
try:
|
|
1875
|
+
if registry == "pypi":
|
|
1876
|
+
url = f"https://pypi.org/pypi/{query}/json"
|
|
1877
|
+
try:
|
|
1878
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Tina4-DevAdmin/1.0"})
|
|
1879
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
1880
|
+
data = json.loads(r.read())
|
|
1881
|
+
info = data.get("info", {})
|
|
1882
|
+
packages.append({
|
|
1883
|
+
"name": info.get("name", query),
|
|
1884
|
+
"description": (info.get("summary") or "")[:120],
|
|
1885
|
+
"version": info.get("version", ""),
|
|
1886
|
+
})
|
|
1887
|
+
except Exception:
|
|
1888
|
+
# Fallback: search API
|
|
1889
|
+
url = f"https://pypi.org/search/?q={urllib.parse.quote(query)}&o="
|
|
1890
|
+
# Simple search not available via JSON — just return empty
|
|
1891
|
+
pass
|
|
1892
|
+
|
|
1893
|
+
# Also try search via simple JSON API
|
|
1894
|
+
if not packages:
|
|
1895
|
+
search_url = f"https://pypi.org/simple/"
|
|
1896
|
+
# PyPI doesn't have a search JSON API, use the name directly
|
|
1897
|
+
pass
|
|
1898
|
+
|
|
1899
|
+
elif registry == "npm":
|
|
1900
|
+
url = f"https://registry.npmjs.org/-/v1/search?text={urllib.parse.quote(query)}&size=10"
|
|
1901
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Tina4-DevAdmin/1.0"})
|
|
1902
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
1903
|
+
data = json.loads(r.read())
|
|
1904
|
+
for obj in data.get("objects", []):
|
|
1905
|
+
pkg = obj.get("package", {})
|
|
1906
|
+
packages.append({
|
|
1907
|
+
"name": pkg.get("name", ""),
|
|
1908
|
+
"description": (pkg.get("description") or "")[:120],
|
|
1909
|
+
"version": pkg.get("version", ""),
|
|
1910
|
+
})
|
|
1911
|
+
|
|
1912
|
+
elif registry == "packagist":
|
|
1913
|
+
url = f"https://packagist.org/search.json?q={urllib.parse.quote(query)}&per_page=10"
|
|
1914
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Tina4-DevAdmin/1.0"})
|
|
1915
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
1916
|
+
data = json.loads(r.read())
|
|
1917
|
+
for pkg in data.get("results", []):
|
|
1918
|
+
packages.append({
|
|
1919
|
+
"name": pkg.get("name", ""),
|
|
1920
|
+
"description": (pkg.get("description") or "")[:120],
|
|
1921
|
+
"version": "",
|
|
1922
|
+
})
|
|
1923
|
+
|
|
1924
|
+
elif registry == "rubygems":
|
|
1925
|
+
url = f"https://rubygems.org/api/v1/search.json?query={urllib.parse.quote(query)}&page=1"
|
|
1926
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Tina4-DevAdmin/1.0"})
|
|
1927
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
1928
|
+
data = json.loads(r.read())
|
|
1929
|
+
for pkg in data[:10]:
|
|
1930
|
+
packages.append({
|
|
1931
|
+
"name": pkg.get("name", ""),
|
|
1932
|
+
"description": (pkg.get("info") or "")[:120],
|
|
1933
|
+
"version": pkg.get("version", ""),
|
|
1934
|
+
})
|
|
1935
|
+
|
|
1936
|
+
elif registry == "crates":
|
|
1937
|
+
url = f"https://crates.io/api/v1/crates?q={urllib.parse.quote(query)}&per_page=10"
|
|
1938
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Tina4-DevAdmin/1.0"})
|
|
1939
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
1940
|
+
data = json.loads(r.read())
|
|
1941
|
+
for pkg in data.get("crates", []):
|
|
1942
|
+
packages.append({
|
|
1943
|
+
"name": pkg.get("name", ""),
|
|
1944
|
+
"description": (pkg.get("description") or "")[:120],
|
|
1945
|
+
"version": pkg.get("max_version", ""),
|
|
1946
|
+
})
|
|
1947
|
+
|
|
1948
|
+
except Exception as e:
|
|
1949
|
+
return response({"packages": [], "error": str(e)})
|
|
1950
|
+
|
|
1951
|
+
return response({"packages": packages})
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
async def _api_deps_install(request, response):
|
|
1955
|
+
"""Install a package dependency.
|
|
1956
|
+
|
|
1957
|
+
Body: { "name": "...", "version": "...", "registry": "pypi|npm|...", "file": "..." }
|
|
1958
|
+
"""
|
|
1959
|
+
import subprocess
|
|
1960
|
+
body = request.body or {}
|
|
1961
|
+
name = body.get("name", "").strip()
|
|
1962
|
+
version = body.get("version", "").strip()
|
|
1963
|
+
registry = body.get("registry", "pypi")
|
|
1964
|
+
|
|
1965
|
+
if not name:
|
|
1966
|
+
return response({"error": "name required"}, 400)
|
|
1967
|
+
|
|
1968
|
+
try:
|
|
1969
|
+
if registry == "pypi":
|
|
1970
|
+
pkg = f"{name}>={version}" if version else name
|
|
1971
|
+
result = subprocess.run(
|
|
1972
|
+
["pip", "install", pkg],
|
|
1973
|
+
capture_output=True, text=True, timeout=60
|
|
1974
|
+
)
|
|
1975
|
+
if result.returncode != 0:
|
|
1976
|
+
return response({"error": result.stderr.strip()}, 500)
|
|
1977
|
+
return response({"message": f"Installed {name} {version}".strip(), "output": result.stdout})
|
|
1978
|
+
|
|
1979
|
+
elif registry == "npm":
|
|
1980
|
+
pkg = f"{name}@{version}" if version else name
|
|
1981
|
+
result = subprocess.run(
|
|
1982
|
+
["npm", "install", pkg],
|
|
1983
|
+
capture_output=True, text=True, timeout=60
|
|
1984
|
+
)
|
|
1985
|
+
if result.returncode != 0:
|
|
1986
|
+
return response({"error": result.stderr.strip()}, 500)
|
|
1987
|
+
return response({"message": f"Installed {name}", "output": result.stdout})
|
|
1988
|
+
|
|
1989
|
+
elif registry == "packagist":
|
|
1990
|
+
pkg = f"{name}:{version}" if version else name
|
|
1991
|
+
result = subprocess.run(
|
|
1992
|
+
["composer", "require", pkg],
|
|
1993
|
+
capture_output=True, text=True, timeout=60
|
|
1994
|
+
)
|
|
1995
|
+
if result.returncode != 0:
|
|
1996
|
+
return response({"error": result.stderr.strip()}, 500)
|
|
1997
|
+
return response({"message": f"Installed {name}", "output": result.stdout})
|
|
1998
|
+
|
|
1999
|
+
elif registry == "rubygems":
|
|
2000
|
+
result = subprocess.run(
|
|
2001
|
+
["gem", "install", name, "-v", version] if version else ["gem", "install", name],
|
|
2002
|
+
capture_output=True, text=True, timeout=60
|
|
2003
|
+
)
|
|
2004
|
+
if result.returncode != 0:
|
|
2005
|
+
return response({"error": result.stderr.strip()}, 500)
|
|
2006
|
+
return response({"message": f"Installed {name}", "output": result.stdout})
|
|
2007
|
+
|
|
2008
|
+
else:
|
|
2009
|
+
return response({"error": f"Unsupported registry: {registry}"}, 400)
|
|
2010
|
+
|
|
2011
|
+
except subprocess.TimeoutExpired:
|
|
2012
|
+
return response({"error": "Install timed out (60s)"}, 500)
|
|
2013
|
+
except FileNotFoundError as e:
|
|
2014
|
+
return response({"error": f"Package manager not found: {e}"}, 500)
|
|
2015
|
+
except Exception as e:
|
|
2016
|
+
return response({"error": str(e)}, 500)
|
|
2017
|
+
|
|
2018
|
+
|
|
2019
|
+
async def _api_git_status(request, response):
|
|
2020
|
+
"""Return git branch, changed files, and summary."""
|
|
2021
|
+
import os, subprocess
|
|
2022
|
+
base = os.getcwd()
|
|
2023
|
+
|
|
2024
|
+
result = {"branch": "", "changes": [], "clean": True}
|
|
2025
|
+
|
|
2026
|
+
try:
|
|
2027
|
+
out = subprocess.run(
|
|
2028
|
+
["git", "branch", "--show-current"],
|
|
2029
|
+
capture_output=True, text=True, cwd=base, timeout=5
|
|
2030
|
+
)
|
|
2031
|
+
result["branch"] = out.stdout.strip()
|
|
2032
|
+
except Exception:
|
|
2033
|
+
return response(result)
|
|
2034
|
+
|
|
2035
|
+
try:
|
|
2036
|
+
out = subprocess.run(
|
|
2037
|
+
["git", "status", "--porcelain", "-uall"],
|
|
2038
|
+
capture_output=True, text=True, cwd=base, timeout=5
|
|
2039
|
+
)
|
|
2040
|
+
for line in out.stdout.strip().split("\n"):
|
|
2041
|
+
if len(line) >= 4:
|
|
2042
|
+
code = line[:2].strip()
|
|
2043
|
+
path = line[3:].strip()
|
|
2044
|
+
status = "modified"
|
|
2045
|
+
if code == "??":
|
|
2046
|
+
status = "untracked"
|
|
2047
|
+
elif "A" in code:
|
|
2048
|
+
status = "added"
|
|
2049
|
+
elif "D" in code:
|
|
2050
|
+
status = "deleted"
|
|
2051
|
+
result["changes"].append({"path": path, "status": status})
|
|
2052
|
+
result["clean"] = len(result["changes"]) == 0
|
|
2053
|
+
except Exception:
|
|
2054
|
+
pass
|
|
2055
|
+
|
|
2056
|
+
return response(result)
|
|
2057
|
+
|
|
2058
|
+
|
|
1502
2059
|
__all__ = ["MessageLog", "RequestInspector", "BrokenTracker",
|
|
1503
2060
|
"get_api_handlers", "render_dev_toolbar"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.11.13 → tina4_python-3.11.14}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|