From 94d160223d284de21a8ef80472f0fba5d66a4705 Mon Sep 17 00:00:00 2001 From: ishenwei Date: Wed, 29 Apr 2026 17:45:22 +0800 Subject: [PATCH] feat: complete Phase 3 scaffolding (templates, static, Docker, per-app skeletons) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Per-app skeleton completion: property/client/setting now have services/, tasks.py, views.py, urls.py, serializers.py, templates//, tests/ - admin.py added to all 10 apps (per spec §2.108 / §17.3) - Top-level templates/: base.html, layouts/{app,auth}.html, components/ {topbar,sidebar,pagination,toast,modal,empty-state}.html, errors/ {403,404,500}.html - static/: css/main.css (Tailwind entry), js/main.js (HTMX toast + CSRF wiring per §7.4), vendor/.gitkeep - tailwind.config.js: Primary teal + neutral slate + semantic colors, Inter font stack, z-60/z-70, shadow-xs, slide-in-right animation per UI_SYSTEM §2.7/§10.1 - package.json: tailwindcss-only build/watch - Dockerfile + docker-compose.yml (6 services: web/db/redis/celery/ celery-beat/tailwind) + docker-compose.prod.yml + Makefile - tests/ root: conftest.py with TenantClient fixture per §720, integration/{property,client,release}/, e2e/, schemathesis skeleton - Removed empty apps/tenant/models/ (tenant uses models.py per §17.1) Validated: manage.py check passes; tree matches spec §2 exactly. --- Dockerfile | 16 ++++ Makefile | 23 ++++++ apps/account/admin.py | 0 apps/client/admin.py | 0 apps/client/serializers.py | 0 apps/client/services/__init__.py | 0 apps/client/tasks.py | 0 apps/client/templates/client/.gitkeep | 0 apps/client/tests/__init__.py | 0 apps/client/urls.py | 5 ++ apps/client/views.py | 0 apps/complex/admin.py | 0 apps/org/admin.py | 0 apps/permission/admin.py | 0 apps/property/admin.py | 0 apps/property/serializers.py | 0 apps/property/services/__init__.py | 0 apps/property/tasks.py | 0 apps/property/templates/property/.gitkeep | 0 apps/property/tests/__init__.py | 0 apps/property/urls.py | 5 ++ apps/property/views.py | 0 apps/region/admin.py | 0 apps/release/admin.py | 0 apps/release/tests/__init__.py | 0 apps/setting/admin.py | 0 apps/setting/serializers.py | 0 apps/setting/services/__init__.py | 0 apps/setting/tasks.py | 0 apps/setting/templates/setting/.gitkeep | 0 apps/setting/tests/__init__.py | 0 apps/setting/urls.py | 5 ++ apps/setting/views.py | 0 apps/tenant/admin.py | 0 apps/tenant/tests/__init__.py | 0 docker-compose.prod.yml | 57 ++++++++++++++ docker-compose.yml | 78 +++++++++++++++++++ package.json | 12 +++ static/css/main.css | 3 + static/js/main.js | 32 ++++++++ static/vendor/.gitkeep | 0 tailwind.config.js | 61 +++++++++++++++ templates/base.html | 25 ++++++ templates/components/empty-state.html | 3 + templates/components/modal.html | 9 +++ templates/components/pagination.html | 3 + templates/components/sidebar.html | 6 ++ templates/components/toast.html | 4 + templates/components/topbar.html | 15 ++++ templates/errors/403.html | 10 +++ templates/errors/404.html | 10 +++ templates/errors/500.html | 10 +++ templates/layouts/app.html | 38 +++++++++ templates/layouts/auth.html | 9 +++ tests/__init__.py | 0 tests/conftest.py | 9 +++ tests/e2e/.gitkeep | 0 tests/e2e/__init__.py | 0 tests/integration/__init__.py | 0 tests/integration/client/.gitkeep | 0 tests/integration/client/__init__.py | 0 tests/integration/property/.gitkeep | 0 tests/integration/property/__init__.py | 0 tests/integration/release/__init__.py | 0 .../release/test_client_update_api.py | 6 ++ 65 files changed, 454 insertions(+) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 apps/account/admin.py create mode 100644 apps/client/admin.py create mode 100644 apps/client/serializers.py create mode 100644 apps/client/services/__init__.py create mode 100644 apps/client/tasks.py create mode 100644 apps/client/templates/client/.gitkeep create mode 100644 apps/client/tests/__init__.py create mode 100644 apps/client/urls.py create mode 100644 apps/client/views.py create mode 100644 apps/complex/admin.py create mode 100644 apps/org/admin.py create mode 100644 apps/permission/admin.py create mode 100644 apps/property/admin.py create mode 100644 apps/property/serializers.py create mode 100644 apps/property/services/__init__.py create mode 100644 apps/property/tasks.py create mode 100644 apps/property/templates/property/.gitkeep create mode 100644 apps/property/tests/__init__.py create mode 100644 apps/property/urls.py create mode 100644 apps/property/views.py create mode 100644 apps/region/admin.py create mode 100644 apps/release/admin.py create mode 100644 apps/release/tests/__init__.py create mode 100644 apps/setting/admin.py create mode 100644 apps/setting/serializers.py create mode 100644 apps/setting/services/__init__.py create mode 100644 apps/setting/tasks.py create mode 100644 apps/setting/templates/setting/.gitkeep create mode 100644 apps/setting/tests/__init__.py create mode 100644 apps/setting/urls.py create mode 100644 apps/setting/views.py create mode 100644 apps/tenant/admin.py create mode 100644 apps/tenant/tests/__init__.py create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 static/css/main.css create mode 100644 static/js/main.js create mode 100644 static/vendor/.gitkeep create mode 100644 tailwind.config.js create mode 100644 templates/base.html create mode 100644 templates/components/empty-state.html create mode 100644 templates/components/modal.html create mode 100644 templates/components/pagination.html create mode 100644 templates/components/sidebar.html create mode 100644 templates/components/toast.html create mode 100644 templates/components/topbar.html create mode 100644 templates/errors/403.html create mode 100644 templates/errors/404.html create mode 100644 templates/errors/500.html create mode 100644 templates/layouts/app.html create mode 100644 templates/layouts/auth.html create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/e2e/.gitkeep create mode 100644 tests/e2e/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/client/.gitkeep create mode 100644 tests/integration/client/__init__.py create mode 100644 tests/integration/property/.gitkeep create mode 100644 tests/integration/property/__init__.py create mode 100644 tests/integration/release/__init__.py create mode 100644 tests/integration/release/test_client_update_api.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8de136a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + libpq-dev gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements/base.txt requirements/base.txt +RUN pip install --no-cache-dir -r requirements/base.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "config.asgi:application", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28b2e43 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: dev migrate shell createsuperuser test lint tailwind-build + +dev: + docker compose up + +migrate: + docker compose exec web python manage.py migrate_schemas --shared + docker compose exec web python manage.py migrate_schemas + +shell: + docker compose exec web python manage.py shell_plus + +test: + docker compose exec web pytest apps/ -v + +lint: + ruff check . && black --check . + +tailwind-build: + npm run build + +createsuperuser: + docker compose exec web python manage.py create_tenant_superuser diff --git a/apps/account/admin.py b/apps/account/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/admin.py b/apps/client/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/serializers.py b/apps/client/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/services/__init__.py b/apps/client/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/tasks.py b/apps/client/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/templates/client/.gitkeep b/apps/client/templates/client/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/tests/__init__.py b/apps/client/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/urls.py b/apps/client/urls.py new file mode 100644 index 0000000..a5f04de --- /dev/null +++ b/apps/client/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "client" + +urlpatterns: list = [] diff --git a/apps/client/views.py b/apps/client/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/complex/admin.py b/apps/complex/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/org/admin.py b/apps/org/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/permission/admin.py b/apps/permission/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/admin.py b/apps/property/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/serializers.py b/apps/property/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/services/__init__.py b/apps/property/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/tasks.py b/apps/property/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/templates/property/.gitkeep b/apps/property/templates/property/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/tests/__init__.py b/apps/property/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/urls.py b/apps/property/urls.py new file mode 100644 index 0000000..c2e559d --- /dev/null +++ b/apps/property/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "property" + +urlpatterns: list = [] diff --git a/apps/property/views.py b/apps/property/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/region/admin.py b/apps/region/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/release/admin.py b/apps/release/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/release/tests/__init__.py b/apps/release/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/admin.py b/apps/setting/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/serializers.py b/apps/setting/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/services/__init__.py b/apps/setting/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/tasks.py b/apps/setting/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/templates/setting/.gitkeep b/apps/setting/templates/setting/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/tests/__init__.py b/apps/setting/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/urls.py b/apps/setting/urls.py new file mode 100644 index 0000000..69d71a1 --- /dev/null +++ b/apps/setting/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "setting" + +urlpatterns: list = [] diff --git a/apps/setting/views.py b/apps/setting/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tenant/admin.py b/apps/tenant/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tenant/tests/__init__.py b/apps/tenant/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..9033e28 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,57 @@ +services: + web: + build: . + command: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 4 + env_file: .env + depends_on: + - db + - redis + networks: + - fonrey_net + + db: + image: postgres:16-alpine + env_file: .env + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - fonrey_db_data:/var/lib/postgresql/data + networks: + - fonrey_net + + redis: + image: redis:7-alpine + volumes: + - fonrey_redis_data:/data + networks: + - fonrey_net + + celery: + build: . + command: celery -A config worker -l info --concurrency 4 + env_file: .env + depends_on: + - db + - redis + networks: + - fonrey_net + + celery-beat: + build: . + command: celery -A config beat -l info + env_file: .env + depends_on: + - db + - redis + networks: + - fonrey_net + +volumes: + fonrey_db_data: + fonrey_redis_data: + +networks: + fonrey_net: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..38c86bb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,78 @@ +services: + web: + build: . + command: uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload + ports: + - "8000:8000" + env_file: .env + volumes: + - .:/app + depends_on: + - db + - redis + networks: + - fonrey_net + + db: + image: postgres:16-alpine + ports: + - "5432:5432" + env_file: .env + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - fonrey_db_data:/var/lib/postgresql/data + networks: + - fonrey_net + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - fonrey_redis_data:/data + networks: + - fonrey_net + + celery: + build: . + command: celery -A config worker -l info + env_file: .env + volumes: + - .:/app + depends_on: + - db + - redis + networks: + - fonrey_net + + celery-beat: + build: . + command: celery -A config beat -l info + env_file: .env + volumes: + - .:/app + depends_on: + - db + - redis + networks: + - fonrey_net + + tailwind: + image: node:20-alpine + working_dir: /app + command: sh -c "npm install && npm run watch" + volumes: + - .:/app + networks: + - fonrey_net + +volumes: + fonrey_db_data: + fonrey_redis_data: + +networks: + fonrey_net: + driver: bridge diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ac8260 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "fonrey-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --minify", + "watch": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --watch" + }, + "devDependencies": { + "tailwindcss": "^3.4.0" + } +} diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..2893674 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,32 @@ +document.body.addEventListener("htmx:afterRequest", function (event) { + var trigger = event.detail.xhr.getResponseHeader("HX-Trigger"); + if (!trigger) return; + + try { + var payload = JSON.parse(trigger); + var toast = payload["fonrey:toast"]; + if (!toast) return; + + var container = document.getElementById("toast-container"); + if (!container) return; + + var node = document.createElement("div"); + node.className = + "bg-white border border-neutral-200 rounded-lg shadow-xs px-4 py-3 min-w-[280px]"; + node.setAttribute("data-toast-type", toast.type || "info"); + node.textContent = toast.message || ""; + container.appendChild(node); + + setTimeout(function () { + node.remove(); + }, 4000); + } catch (e) { + } +}); + +document.body.addEventListener("htmx:configRequest", function (event) { + var meta = document.querySelector('meta[name="csrf-token"]'); + if (meta) { + event.detail.headers["X-CSRFToken"] = meta.getAttribute("content"); + } +}); diff --git a/static/vendor/.gitkeep b/static/vendor/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..255f3a3 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,61 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./templates/**/*.html", + "./apps/**/templates/**/*.html", + "./static/js/**/*.js", + ], + theme: { + extend: { + colors: { + primary: { + 50: "#F0FDFA", + 100: "#CCFBF1", + 200: "#99F6E4", + 300: "#5EEAD4", + 400: "#2DD4BF", + 500: "#14B8A6", + 600: "#0F766E", + 700: "#115E59", + 800: "#134E4A", + }, + neutral: { + 50: "#F8FAFC", + 100: "#F1F5F9", + 200: "#E2E8F0", + 300: "#CBD5E1", + 400: "#94A3B8", + 500: "#64748B", + 600: "#475569", + 700: "#334155", + 800: "#1E293B", + 900: "#0F172A", + }, + success: { 600: "#16A34A" }, + warning: { 600: "#D97706" }, + danger: { 600: "#DC2626" }, + info: { 600: "#2563EB" }, + }, + fontFamily: { + sans: ["Inter", "PingFang SC", "Microsoft YaHei", "sans-serif"], + }, + zIndex: { + 60: "60", + 70: "70", + }, + boxShadow: { + xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)", + }, + keyframes: { + "slide-in-right": { + "0%": { transform: "translateX(100%)", opacity: "0" }, + "100%": { transform: "translateX(0)", opacity: "1" }, + }, + }, + animation: { + "slide-in-right": "slide-in-right 0.2s ease-out", + }, + }, + }, + plugins: [], +}; diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..e324496 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,25 @@ +{% load static %} + + + + + + + {% block title %}Fonrey{% endblock %} + + + + {% block extra_head %}{% endblock %} + + + + + + + {% block content %}{% endblock %} + +
+ + {% block extra_js %}{% endblock %} + + diff --git a/templates/components/empty-state.html b/templates/components/empty-state.html new file mode 100644 index 0000000..9cf6ddb --- /dev/null +++ b/templates/components/empty-state.html @@ -0,0 +1,3 @@ +
+

{% block empty_message %}暂无数据{% endblock %}

+
diff --git a/templates/components/modal.html b/templates/components/modal.html new file mode 100644 index 0000000..3f5e5e7 --- /dev/null +++ b/templates/components/modal.html @@ -0,0 +1,9 @@ +
+
+ {% block modal_body %}{% endblock %} +
+
diff --git a/templates/components/pagination.html b/templates/components/pagination.html new file mode 100644 index 0000000..85a3e0e --- /dev/null +++ b/templates/components/pagination.html @@ -0,0 +1,3 @@ + diff --git a/templates/components/sidebar.html b/templates/components/sidebar.html new file mode 100644 index 0000000..26f763d --- /dev/null +++ b/templates/components/sidebar.html @@ -0,0 +1,6 @@ + diff --git a/templates/components/toast.html b/templates/components/toast.html new file mode 100644 index 0000000..8bcbe3a --- /dev/null +++ b/templates/components/toast.html @@ -0,0 +1,4 @@ + diff --git a/templates/components/topbar.html b/templates/components/topbar.html new file mode 100644 index 0000000..7d60a0a --- /dev/null +++ b/templates/components/topbar.html @@ -0,0 +1,15 @@ +
+
+ Fonrey +
+ + + +
+ + + +
+
diff --git a/templates/errors/403.html b/templates/errors/403.html new file mode 100644 index 0000000..f9be568 --- /dev/null +++ b/templates/errors/403.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block title %}403 - 无权限{% endblock %} +{% block content %} +
+
+

403

+

您没有访问该资源的权限

+
+
+{% endblock %} diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 0000000..57ea23f --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block title %}404 - 页面未找到{% endblock %} +{% block content %} +
+
+

404

+

页面未找到

+
+
+{% endblock %} diff --git a/templates/errors/500.html b/templates/errors/500.html new file mode 100644 index 0000000..5cb6edc --- /dev/null +++ b/templates/errors/500.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block title %}500 - 服务器错误{% endblock %} +{% block content %} +
+
+

500

+

服务器内部错误

+
+
+{% endblock %} diff --git a/templates/layouts/app.html b/templates/layouts/app.html new file mode 100644 index 0000000..10dd435 --- /dev/null +++ b/templates/layouts/app.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block body_class %}bg-neutral-50 text-neutral-900{% endblock %} + +{% block content %} +
+ {% include "components/topbar.html" %} + +
+ {% include "components/sidebar.html" %} + +
+ {% block main %}{% endblock %} +
+
+
+ + + +{% endblock %} diff --git a/templates/layouts/auth.html b/templates/layouts/auth.html new file mode 100644 index 0000000..6d3beb4 --- /dev/null +++ b/templates/layouts/auth.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block body_class %}bg-neutral-50 min-h-screen flex items-center justify-center{% endblock %} + +{% block content %} +
+ {% block auth_content %}{% endblock %} +
+{% endblock %} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0482f14 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from django_tenants.test.client import TenantClient +from django_tenants.utils import schema_context + + +@pytest.fixture +def tenant_client(db, tenant): + with schema_context(tenant.schema_name): + yield TenantClient(tenant) diff --git a/tests/e2e/.gitkeep b/tests/e2e/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/client/.gitkeep b/tests/integration/client/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/client/__init__.py b/tests/integration/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/property/.gitkeep b/tests/integration/property/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/property/__init__.py b/tests/integration/property/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/release/__init__.py b/tests/integration/release/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/release/test_client_update_api.py b/tests/integration/release/test_client_update_api.py new file mode 100644 index 0000000..cbe5d51 --- /dev/null +++ b/tests/integration/release/test_client_update_api.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.mark.skip(reason="skeleton - implement after release endpoints are wired") +def test_client_update_api_contract(): + pass