shaapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. shaapi/__init__.py +3 -0
  2. shaapi/cli.py +97 -0
  3. shaapi/generator.py +114 -0
  4. shaapi/template/.dockerignore +37 -0
  5. shaapi/template/.env.template +59 -0
  6. shaapi/template/.gitattributes +12 -0
  7. shaapi/template/.gitignore +170 -0
  8. shaapi/template/.gitlab-ci.yml +89 -0
  9. shaapi/template/Dockerfile +59 -0
  10. shaapi/template/LICENSE +21 -0
  11. shaapi/template/README.md +206 -0
  12. shaapi/template/backend/.gitignore +164 -0
  13. shaapi/template/backend/__init__.py +0 -0
  14. shaapi/template/backend/alembic/README +1 -0
  15. shaapi/template/backend/alembic/env.py +102 -0
  16. shaapi/template/backend/alembic/script.py.mako +26 -0
  17. shaapi/template/backend/alembic/versions/2026_06_08_1024-64524c63b666_initial.py +143 -0
  18. shaapi/template/backend/alembic.ini +117 -0
  19. shaapi/template/backend/app/__init__.py +55 -0
  20. shaapi/template/backend/app/admin/__init__.py +1 -0
  21. shaapi/template/backend/app/admin/api/v1/__init__.py +0 -0
  22. shaapi/template/backend/app/admin/api/v1/auth.py +59 -0
  23. shaapi/template/backend/app/admin/api/v1/casbin.py +218 -0
  24. shaapi/template/backend/app/admin/api/v1/login_log.py +63 -0
  25. shaapi/template/backend/app/admin/api/v1/opera_log.py +61 -0
  26. shaapi/template/backend/app/admin/api/v1/role.py +108 -0
  27. shaapi/template/backend/app/admin/api/v1/user.py +47 -0
  28. shaapi/template/backend/app/admin/schema/casbin_rule.py +45 -0
  29. shaapi/template/backend/app/admin/schema/login_log.py +36 -0
  30. shaapi/template/backend/app/admin/schema/opera_log.py +43 -0
  31. shaapi/template/backend/app/admin/schema/role.py +36 -0
  32. shaapi/template/backend/app/admin/schema/sso.py +37 -0
  33. shaapi/template/backend/app/admin/schema/token.py +74 -0
  34. shaapi/template/backend/app/admin/schema/user.py +93 -0
  35. shaapi/template/backend/app/admin/service/auth_service.py +233 -0
  36. shaapi/template/backend/app/admin/service/casbin_service.py +135 -0
  37. shaapi/template/backend/app/admin/service/login_log_service.py +62 -0
  38. shaapi/template/backend/app/admin/service/opera_log_service.py +31 -0
  39. shaapi/template/backend/app/admin/service/role_service.py +79 -0
  40. shaapi/template/backend/app/admin/service/secure_token_service.py +60 -0
  41. shaapi/template/backend/app/admin/service/user_service.py +153 -0
  42. shaapi/template/backend/app/api.py +11 -0
  43. shaapi/template/backend/common/__init__.py +0 -0
  44. shaapi/template/backend/common/cloud_storage/__init__.py +11 -0
  45. shaapi/template/backend/common/cloud_storage/cloud_storage.py +180 -0
  46. shaapi/template/backend/common/dataclasses.py +52 -0
  47. shaapi/template/backend/common/email_conf/email.py +105 -0
  48. shaapi/template/backend/common/enums.py +144 -0
  49. shaapi/template/backend/common/exception/__init__.py +0 -0
  50. shaapi/template/backend/common/exception/errors.py +87 -0
  51. shaapi/template/backend/common/exception/exception_handler.py +280 -0
  52. shaapi/template/backend/common/log.py +123 -0
  53. shaapi/template/backend/common/model.py +68 -0
  54. shaapi/template/backend/common/pagination.py +83 -0
  55. shaapi/template/backend/common/response/__init__.py +0 -0
  56. shaapi/template/backend/common/response/response_code.py +158 -0
  57. shaapi/template/backend/common/response/response_schema.py +110 -0
  58. shaapi/template/backend/common/schema.py +144 -0
  59. shaapi/template/backend/common/security/jwt.py +203 -0
  60. shaapi/template/backend/common/security/rbac.py +98 -0
  61. shaapi/template/backend/common/security/sec_token.py +6 -0
  62. shaapi/template/backend/common/socketio/action.py +11 -0
  63. shaapi/template/backend/common/socketio/server.py +50 -0
  64. shaapi/template/backend/common/sso/base.py +69 -0
  65. shaapi/template/backend/common/sso/google.py +127 -0
  66. shaapi/template/backend/core/conf.py +208 -0
  67. shaapi/template/backend/core/path_conf.py +24 -0
  68. shaapi/template/backend/core/registrar.py +195 -0
  69. shaapi/template/backend/crud/__init__.py +1 -0
  70. shaapi/template/backend/crud/crud_base.py +35 -0
  71. shaapi/template/backend/crud/crud_casbin.py +46 -0
  72. shaapi/template/backend/crud/crud_login_log.py +58 -0
  73. shaapi/template/backend/crud/crud_opera_log.py +58 -0
  74. shaapi/template/backend/crud/crud_role.py +128 -0
  75. shaapi/template/backend/crud/crud_user.py +267 -0
  76. shaapi/template/backend/database/__init__.py +0 -0
  77. shaapi/template/backend/database/db_postgres.py +125 -0
  78. shaapi/template/backend/database/db_redis.py +62 -0
  79. shaapi/template/backend/entrypoint-api.sh +19 -0
  80. shaapi/template/backend/lang/en/app.py +18 -0
  81. shaapi/template/backend/lang/en/auth.py +10 -0
  82. shaapi/template/backend/lang/fr/app.py +18 -0
  83. shaapi/template/backend/lang/fr/auth.py +10 -0
  84. shaapi/template/backend/main.py +54 -0
  85. shaapi/template/backend/middleware/__init__.py +1 -0
  86. shaapi/template/backend/middleware/access_middleware.py +19 -0
  87. shaapi/template/backend/middleware/i18n_middleware.py +19 -0
  88. shaapi/template/backend/middleware/jwt_auth_middleware.py +73 -0
  89. shaapi/template/backend/middleware/opera_log_middleware.py +179 -0
  90. shaapi/template/backend/middleware/state_middleware.py +26 -0
  91. shaapi/template/backend/models/__init__.py +10 -0
  92. shaapi/template/backend/models/associations.py +20 -0
  93. shaapi/template/backend/models/casbin_rule.py +30 -0
  94. shaapi/template/backend/models/login_log.py +28 -0
  95. shaapi/template/backend/models/opera_log.py +36 -0
  96. shaapi/template/backend/models/role.py +27 -0
  97. shaapi/template/backend/models/user.py +30 -0
  98. shaapi/template/backend/seeder/json/admin.json +15 -0
  99. shaapi/template/backend/seeder/json/user.json +15 -0
  100. shaapi/template/backend/seeder/run.py +34 -0
  101. shaapi/template/backend/static/ip2region.xdb +0 -0
  102. shaapi/template/backend/templates/build/meet.html +169 -0
  103. shaapi/template/backend/templates/build/new_account.html +373 -0
  104. shaapi/template/backend/templates/build/reset-password.html +170 -0
  105. shaapi/template/backend/templates/build/test_email.html +25 -0
  106. shaapi/template/backend/templates/build/welcome-one-1.html +160 -0
  107. shaapi/template/backend/templates/build/welcome-one.html +178 -0
  108. shaapi/template/backend/templates/build/welcome-two.html +234 -0
  109. shaapi/template/backend/templates/index.html +0 -0
  110. shaapi/template/backend/templates/src/new_account.mjml +15 -0
  111. shaapi/template/backend/templates/src/reset_password.mjml +19 -0
  112. shaapi/template/backend/templates/src/test_email.mjml +11 -0
  113. shaapi/template/backend/templates/ws/ws.html +70 -0
  114. shaapi/template/backend/utils/demo_site.py +18 -0
  115. shaapi/template/backend/utils/encrypt.py +108 -0
  116. shaapi/template/backend/utils/health_check.py +34 -0
  117. shaapi/template/backend/utils/prometheus.py +135 -0
  118. shaapi/template/backend/utils/request_parse.py +110 -0
  119. shaapi/template/backend/utils/serializers.py +75 -0
  120. shaapi/template/backend/utils/timezone.py +51 -0
  121. shaapi/template/backend/utils/trace_id.py +7 -0
  122. shaapi/template/backend/utils/translator.py +28 -0
  123. shaapi/template/devops/scripts/deploy.sh +7 -0
  124. shaapi/template/devops/scripts/setup_env.sh +62 -0
  125. shaapi/template/docker-compose.monitoring.yml +63 -0
  126. shaapi/template/docker-compose.override.yml +12 -0
  127. shaapi/template/docker-compose.yml +90 -0
  128. shaapi/template/docker-run.sh +99 -0
  129. shaapi/template/etc/dashboards/fastapi-observability.json +1044 -0
  130. shaapi/template/etc/dashboards.yaml +10 -0
  131. shaapi/template/etc/grafana/datasource.yml +79 -0
  132. shaapi/template/etc/prometheus/prometheus.yml +52 -0
  133. shaapi/template/package-lock.json +2102 -0
  134. shaapi/template/package.json +16 -0
  135. shaapi/template/pyproject.toml +78 -0
  136. shaapi/template/uv.lock +2866 -0
  137. shaapi-0.1.0.dist-info/METADATA +92 -0
  138. shaapi-0.1.0.dist-info/RECORD +141 -0
  139. shaapi-0.1.0.dist-info/WHEEL +4 -0
  140. shaapi-0.1.0.dist-info/entry_points.txt +2 -0
  141. shaapi-0.1.0.dist-info/licenses/LICENCE +21 -0
@@ -0,0 +1,110 @@
1
+ import httpx
2
+ import json
3
+
4
+ from asgiref.sync import sync_to_async
5
+ from fastapi import Request
6
+ from user_agents import parse
7
+ from XdbSearchIP.xdbSearcher import XdbSearcher
8
+
9
+ from backend.common.dataclasses import IpInfo, UserAgentInfo
10
+ from backend.common.log import log
11
+ from backend.core.conf import settings
12
+ from backend.core.path_conf import IP2REGION_XDB
13
+ from backend.database.db_redis import redis_client
14
+
15
+
16
+ def get_request_ip(request: Request) -> str:
17
+ """Get the ip address of the request"""
18
+ real = request.headers.get('X-Real-IP')
19
+ if real:
20
+ ip = real
21
+ else:
22
+ forwarded = request.headers.get('X-Forwarded-For')
23
+ if forwarded:
24
+ ip = forwarded.split(',')[0]
25
+ else:
26
+ ip = request.client.host
27
+ if ip == 'testclient':
28
+ ip = '127.0.0.1'
29
+ return ip
30
+
31
+
32
+ async def get_location_online(ip: str, user_agent: str) -> dict | None:
33
+ """
34
+ Obtain ip address attributes online, no guarantee of availability, higher accuracy
35
+
36
+ :param ip:
37
+ :param user_agent:
38
+ :return:
39
+ """
40
+ async with httpx.AsyncClient(timeout=3) as client:
41
+ ip_api_url = f'http://ip-api.com/json/{ip}?lang=fr-FR'
42
+ headers = {'User-Agent': user_agent}
43
+ try:
44
+ response = await client.get(ip_api_url, headers=headers)
45
+ if response.status_code == 200:
46
+ return response.json()
47
+ except Exception as e:
48
+ log.error(f'Failed to obtain ip address attributes online, error message:{e}')
49
+ return None
50
+
51
+
52
+ @sync_to_async
53
+ def get_location_offline(ip: str) -> dict | None:
54
+ """
55
+ Get ip address generically offline, can't guarantee accuracy, 100% available
56
+
57
+ :param ip:
58
+ :return:
59
+ """
60
+ try:
61
+ cb = XdbSearcher.loadContentFromFile(dbfile=IP2REGION_XDB)
62
+ searcher = XdbSearcher(contentBuff=cb)
63
+ data = searcher.search(ip)
64
+ searcher.close()
65
+ data = data.split('|')
66
+ return {
67
+ 'country': data[0] if data[0] != '0' else None,
68
+ 'regionName': data[2] if data[2] != '0' else None,
69
+ 'city': data[3] if data[3] != '0' else None,
70
+ }
71
+ except Exception as e:
72
+ log.error(f'Failed to obtain ip address generics offline, error message:{e}')
73
+ return None
74
+
75
+
76
+ async def parse_ip_info(request: Request) -> IpInfo:
77
+ country, region, city = None, None, None
78
+ ip = get_request_ip(request)
79
+ location = await redis_client.get(f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}')
80
+ if location:
81
+ location = json.loads(location)
82
+ country = location.get("country")
83
+ region = location.get("region")
84
+ city = location.get("city")
85
+ return IpInfo(ip=ip, country=country, region=region, city=city)
86
+ if settings.IP_LOCATION_PARSE == 'online':
87
+ location_info = await get_location_online(ip, request.headers.get('User-Agent'))
88
+ elif settings.IP_LOCATION_PARSE == 'offline':
89
+ location_info = await get_location_offline(ip)
90
+ else:
91
+ location_info = None
92
+ if location_info:
93
+ country = location_info.get('country')
94
+ region = location_info.get('regionName')
95
+ city = location_info.get('city')
96
+ await redis_client.set(
97
+ f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}',
98
+ json.dumps({"country": country, "region": region, "city": city}),
99
+ ex=settings.IP_LOCATION_EXPIRE_SECONDS,
100
+ )
101
+ return IpInfo(ip=ip, country=country, region=region, city=city)
102
+
103
+
104
+ def parse_user_agent_info(request: Request) -> UserAgentInfo:
105
+ user_agent = request.headers.get('User-Agent')
106
+ _user_agent = parse(user_agent)
107
+ os = _user_agent.get_os()
108
+ browser = _user_agent.get_browser()
109
+ device = _user_agent.get_device()
110
+ return UserAgentInfo(user_agent=user_agent, device=device, os=os, browser=browser)
@@ -0,0 +1,75 @@
1
+ from decimal import Decimal
2
+ from typing import Any, Sequence, TypeVar
3
+
4
+ from fastapi.encoders import decimal_encoder
5
+ from msgspec import json
6
+ from sqlalchemy import Row, RowMapping
7
+ from sqlalchemy.orm import ColumnProperty, SynonymProperty, class_mapper
8
+ from starlette.responses import JSONResponse
9
+
10
+ RowData = Row | RowMapping | Any
11
+
12
+ R = TypeVar('R', bound=RowData)
13
+
14
+
15
+ def select_columns_serialize(row: R) -> dict:
16
+ """
17
+ Serialize SQLAlchemy select table columns, does not contain relational columns
18
+
19
+ :param row:
20
+ :return:
21
+ """
22
+ result = {}
23
+ for column in row.__table__.columns.keys():
24
+ v = getattr(row, column)
25
+ if isinstance(v, Decimal):
26
+ v = decimal_encoder(v)
27
+ result[column] = v
28
+ return result
29
+
30
+
31
+ def select_list_serialize(row: Sequence[R]) -> list:
32
+ """
33
+ Serialize SQLAlchemy select list
34
+
35
+ :param row:
36
+ :return:
37
+ """
38
+ result = [select_columns_serialize(_) for _ in row]
39
+ return result
40
+
41
+
42
+ def select_as_dict(row: R, use_alias: bool = False) -> dict:
43
+ """
44
+ Converting SQLAlchemy select to dict, which can contain relational data,
45
+ depends on the properties of the select object itself
46
+
47
+ If set use_alias is True, the column name will be returned as alias,
48
+ If alias doesn't exist in columns, we don't recommend setting it to True
49
+
50
+ :param row:
51
+ :param use_alias:
52
+ :return:
53
+ """
54
+ if not use_alias:
55
+ result = row.__dict__
56
+ if '_sa_instance_state' in result:
57
+ del result['_sa_instance_state']
58
+ return result
59
+ else:
60
+ result = {}
61
+ mapper = class_mapper(row.__class__)
62
+ for prop in mapper.iterate_properties:
63
+ if isinstance(prop, (ColumnProperty, SynonymProperty)):
64
+ key = prop.key
65
+ result[key] = getattr(row, key)
66
+ return result
67
+
68
+
69
+ class MsgSpecJSONResponse(JSONResponse):
70
+ """
71
+ JSON response using the high-performance msgspec library to serialize data to JSON.
72
+ """
73
+
74
+ def render(self, content: Any) -> bytes:
75
+ return json.encode(content)
@@ -0,0 +1,51 @@
1
+ import zoneinfo
2
+
3
+ from datetime import datetime
4
+ from datetime import timezone as datetime_timezone
5
+
6
+ from backend.core.conf import settings
7
+
8
+
9
+ class TimeZone:
10
+ def __init__(self, tz: str = settings.DATETIME_TIMEZONE):
11
+ self.tz_info = zoneinfo.ZoneInfo(tz)
12
+
13
+ def now(self) -> datetime:
14
+ """
15
+ Get time zone time
16
+
17
+ :return:
18
+ """
19
+ return datetime.now(self.tz_info)
20
+
21
+ def f_datetime(self, dt: datetime) -> datetime:
22
+ """
23
+ datetime Time to Time Zone Time
24
+
25
+ :param dt:
26
+ :return:
27
+ """
28
+ return dt.astimezone(self.tz_info)
29
+
30
+ def f_str(self, date_str: str, format_str: str = settings.DATETIME_FORMAT) -> datetime:
31
+ """
32
+ Time string to time zone time
33
+
34
+ :param date_str:
35
+ :param format_str:
36
+ :return:
37
+ """
38
+ return datetime.strptime(date_str, format_str).replace(tzinfo=self.tz_info)
39
+
40
+ @staticmethod
41
+ def f_utc(dt: datetime) -> datetime:
42
+ """
43
+ Time Zone Time to UTC (GMT) Time Zone
44
+
45
+ :param dt:
46
+ :return:
47
+ """
48
+ return dt.astimezone(datetime_timezone.utc)
49
+
50
+
51
+ timezone = TimeZone()
@@ -0,0 +1,7 @@
1
+ from fastapi import Request
2
+
3
+ from backend.core.conf import settings
4
+
5
+
6
+ def get_request_trace_id(request: Request) -> str:
7
+ return request.headers.get(settings.TRACE_ID_REQUEST_HEADER_KEY) or '-'
@@ -0,0 +1,28 @@
1
+ import importlib
2
+ from typing import Any, Dict
3
+
4
+
5
+ class Translator:
6
+ _instances: Dict[str, 'Translator'] = {}
7
+
8
+ def __new__(cls, lang: str) -> 'Translator':
9
+ if lang not in cls._instances:
10
+ cls._instances[lang] = super(Translator, cls).__new__(cls)
11
+ return cls._instances[lang]
12
+
13
+ def __init__(self, lang: str):
14
+ self.lang = lang
15
+
16
+ def t(self, key: str, **kwargs: Dict[str, Any]) -> str:
17
+ file_key, *translation_keys = key.split('.')
18
+
19
+ locale_module = importlib.import_module(f'backend.lang.{self.lang}.{file_key}')
20
+
21
+ translation = locale_module.locale
22
+ for translation_key in translation_keys:
23
+ translation = translation.get(translation_key, None)
24
+ if translation is None:
25
+ return f'Key {key} not found in {self.lang} locale'
26
+ if kwargs.keys():
27
+ translation = translation.format(**kwargs)
28
+ return translation
@@ -0,0 +1,7 @@
1
+ ssh -o StrictHostKeyChecking=no "$VPS_USER@$VPS_IP_ADDRESS" << ENDSSH
2
+ cd /app
3
+ export $(cat .env | xargs)
4
+ docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY
5
+ docker pull $API_IMAGE
6
+ docker-compose -f docker-compose.yml up --build -d
7
+ ENDSSH
@@ -0,0 +1,62 @@
1
+ echo CI_JOB_TOKEN=$CI_JOB_TOKEN >> .env
2
+ echo CI_REGISTRY=$CI_REGISTRY >> .env
3
+ echo CI_REGISTRY_USER=$CI_REGISTRY_USER >> .env
4
+
5
+ echo VPS_IP_ADDRESS=$VPS_IP_ADDRESS >> .env
6
+ echo VPS_USER=$VPS_USER >> .env
7
+ echo API_IMAGE=$IMAGE:api >> .env
8
+
9
+
10
+ echo ENVIRONMENT=$ENVIRONMENT >> .env
11
+ echo TITLE=$TITLE >> .env
12
+ echo CLIENT_ROOT_PATH=$CLIENT_ROOT_PATH >> .env
13
+ echo ADMIN_ROOT_PATH=$ADMIN_ROOT_PATH >> .env
14
+ echo CLIENT_URL=$CLIENT_URL >> .env
15
+ echo API_BASE_URL=$API_BASE_URL >> .env
16
+ echo API_URL=$API_URL >> .env
17
+ echo ACCESS_TOKEN_EXPIRE_MINUTES=$ACCESS_TOKEN_EXPIRE_MINUTES >> .env
18
+ # POSTGRES
19
+
20
+ echo POSTGRES_HOST=$POSTGRES_HOST >> .env
21
+ echo POSTGRES_PORT=$POSTGRES_PORT >> .env
22
+ echo POSTGRES_USER=$POSTGRES_USER >> .env
23
+ echo POSTGRES_PASSWORD=$POSTGRES_PASSWORD >> .env
24
+ # Redis
25
+ echo REDIS_HOST=$REDIS_HOST >> .env
26
+ echo REDIS_PORT=$REDIS_PORT >> .env
27
+ echo REDIS_PASSWORD=$REDIS_PASSWORD >> .env
28
+ echo REDIS_DATABASE=$REDIS_DATABASE >> .env
29
+ # Token
30
+ echo TOKEN_SECRET_KEY=$TOKEN_SECRET_KEY >> .env
31
+ # Opera Log
32
+ echo OPERA_LOG_ENCRYPT_SECRET_KEY=$OPERA_LOG_ENCRYPT_SECRET_KEY >> .env
33
+ # Admin
34
+ # OAuth2
35
+ echo OAUTH2_GITHUB_CLIENT_ID=$OAUTH2_GITHUB_CLIENT_ID >> .env
36
+ echo OAUTH2_GITHUB_CLIENT_SECRET=$OAUTH2_GITHUB_CLIENT_SECRET >> .env
37
+ echo OAUTH2_LINUX_DO_CLIENT_ID=$OAUTH2_LINUX_DO_CLIENT_ID >> .env
38
+ echo OAUTH2_LINUX_DO_CLIENT_SECRET=$OAUTH2_LINUX_DO_CLIENT_SECRET >> .env
39
+ # Task
40
+ # Celery
41
+ echo CELERY_BROKER_REDIS_DATABASE=$CELERY_BROKER_REDIS_DATABASE >> .env
42
+ echo CELERY_BACKEND_REDIS_DATABASE=$CELERY_BACKEND_REDIS_DATABASE >> .env
43
+ # Rabbitmq
44
+ echo RABBITMQ_HOST=$RABBITMQ_HOST >> .env
45
+ echo RABBITMQ_PORT=$RABBITMQ_PORT >> .env
46
+ echo RABBITMQ_USERNAME=$RABBITMQ_USERNAME >> .env
47
+ echo RABBITMQ_PASSWORD=$RABBITMQ_PASSWORD >> .env
48
+ # Minio
49
+ echo MINIO_ENDPOINT=$MINIO_ENDPOINT >> .env
50
+ echo MINIO_PORT=$MINIO_PORT >> .env
51
+ echo MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY >> .env
52
+ echo MINIO_SECRET_KEY=$MINIO_SECRET_KEY >> .env
53
+ echo MINIO_BUCKET_NAME=$MINIO_BUCKET_NAME >> .env
54
+ echo MINIO_CLOUD_URL=$MINIO_CLOUD_URL >> .env
55
+
56
+ echo SMTP_TLS=$SMTP_TLS >> .env
57
+ echo SMTP_PORT=$SMTP_PORT >> .env
58
+ echo SMTP_HOST=$SMTP_HOST >> .env
59
+ echo SMTP_USER=$SMTP_USER >> .env
60
+ echo EMAILS_FROM_EMAIL=$EMAILS_FROM_EMAIL >> .env
61
+ echo EMAILS_FROM_NAME=$EMAILS_FROM_NAME >> .env
62
+ echo SMTP_PASSWORD=$SMTP_PASSWORD >> .env
@@ -0,0 +1,63 @@
1
+ # Opt-in observability stack. Enable with: ./docker-run.sh up --monitoring
2
+ # Adds Prometheus (metrics), Tempo (traces), Loki (logs) and Grafana (UI),
3
+ # and rebuilds the API image with the `monitoring` extra + observability on.
4
+ services:
5
+ api:
6
+ build:
7
+ args:
8
+ EXTRAS: "--extra monitoring"
9
+ environment:
10
+ - OBSERVABILITY_ENABLED=true
11
+ - OTLP_GRPC_ENDPOINT=shaapi_tempo:4317
12
+
13
+ shaapi_prometheus:
14
+ image: prom/prometheus:v2.51.2
15
+ container_name: shaapi_prometheus
16
+ ports:
17
+ - "9090:9090"
18
+ volumes:
19
+ - ./etc/prometheus:/workspace
20
+ command:
21
+ - --config.file=/workspace/prometheus.yml
22
+ - --enable-feature=exemplar-storage
23
+ networks:
24
+ - shaapi
25
+
26
+ shaapi_tempo:
27
+ image: grafana/tempo:2.4.1
28
+ container_name: shaapi_tempo
29
+ command: ["--target=all", "--storage.trace.backend=local", "--storage.trace.local.path=/var/tempo", "--auth.enabled=false"]
30
+ ports:
31
+ - "4317:4317"
32
+ - "4318:4318"
33
+ networks:
34
+ - shaapi
35
+
36
+ shaapi_loki:
37
+ image: grafana/loki:3.0.0
38
+ container_name: shaapi_loki
39
+ command: -config.file=/etc/loki/local-config.yaml
40
+ ports:
41
+ - "3100:3100"
42
+ networks:
43
+ - shaapi
44
+
45
+ shaapi_grafana:
46
+ image: grafana/grafana:10.4.2
47
+ container_name: shaapi_grafana
48
+ ports:
49
+ - "3000:3000"
50
+ environment:
51
+ - GF_AUTH_ANONYMOUS_ENABLED=true
52
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
53
+ - GF_AUTH_DISABLE_LOGIN_FORM=true
54
+ volumes:
55
+ - ./etc/grafana/:/etc/grafana/provisioning/datasources
56
+ - ./etc/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml
57
+ - ./etc/dashboards:/etc/grafana/dashboards
58
+ depends_on:
59
+ - shaapi_prometheus
60
+ - shaapi_tempo
61
+ - shaapi_loki
62
+ networks:
63
+ - shaapi
@@ -0,0 +1,12 @@
1
+ # Development overrides — automatically applied by `docker compose up`.
2
+ # Bind-mounts the source so code changes hot-reload (uvicorn --reload) WITHOUT
3
+ # rebuilding the image. The image is only rebuilt when dependencies change.
4
+ #
5
+ # For production, run the base file only (no mount, baked-in code):
6
+ # docker compose -f docker-compose.yml up -d
7
+ # and set ENVIRONMENT=prod in your .env.
8
+ services:
9
+ api:
10
+ volumes:
11
+ # Live source. The venv lives at /opt/venv, so it is not shadowed.
12
+ - .:/app
@@ -0,0 +1,90 @@
1
+ services:
2
+ api:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ image: shaapi:latest
7
+ container_name: shaapi_api
8
+ restart: unless-stopped
9
+ ports:
10
+ - "8000:8000"
11
+ env_file:
12
+ - .env
13
+ environment:
14
+ # Point the app at the compose service names (overrides .env localhost)
15
+ - POSTGRES_HOST=shaapi_postgres
16
+ - REDIS_HOST=shaapi_redis
17
+ - MINIO_ENDPOINT=shaapi_minio:9000
18
+ depends_on:
19
+ postgres:
20
+ condition: service_healthy
21
+ redis:
22
+ condition: service_healthy
23
+ minio:
24
+ condition: service_started
25
+ networks:
26
+ - shaapi
27
+
28
+ postgres:
29
+ image: postgres:16-alpine
30
+ container_name: shaapi_postgres
31
+ restart: unless-stopped
32
+ environment:
33
+ - POSTGRES_USER=${POSTGRES_USER:-postgres}
34
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
35
+ - POSTGRES_DB=${POSTGRES_DATABASE:-shaapi}
36
+ volumes:
37
+ - shaapi_postgres:/var/lib/postgresql/data
38
+ ports:
39
+ - "${POSTGRES_PORT:-5432}:5432"
40
+ healthcheck:
41
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DATABASE:-shaapi}"]
42
+ interval: 5s
43
+ timeout: 5s
44
+ retries: 10
45
+ networks:
46
+ - shaapi
47
+
48
+ redis:
49
+ image: redis:7-alpine
50
+ container_name: shaapi_redis
51
+ restart: unless-stopped
52
+ volumes:
53
+ - shaapi_redis:/data
54
+ healthcheck:
55
+ test: ["CMD", "redis-cli", "ping"]
56
+ interval: 5s
57
+ timeout: 5s
58
+ retries: 10
59
+ networks:
60
+ - shaapi
61
+
62
+ minio:
63
+ image: minio/minio:latest
64
+ container_name: shaapi_minio
65
+ restart: unless-stopped
66
+ command: server /data --console-address ":9001"
67
+ environment:
68
+ - MINIO_ROOT_USER=${MINIO_ACCESS_KEY:-minioadmin}
69
+ - MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY:-minioadmin}
70
+ volumes:
71
+ - shaapi_minio:/data
72
+ ports:
73
+ - "9000:9000"
74
+ - "9001:9001"
75
+ healthcheck:
76
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
77
+ interval: 30s
78
+ timeout: 20s
79
+ retries: 3
80
+ networks:
81
+ - shaapi
82
+
83
+ volumes:
84
+ shaapi_postgres:
85
+ shaapi_redis:
86
+ shaapi_minio:
87
+
88
+ networks:
89
+ shaapi:
90
+ driver: bridge
@@ -0,0 +1,99 @@
1
+ #!/bin/bash
2
+ # Manage the shaapi Docker stack.
3
+ #
4
+ # Usage: ./docker-run.sh [command] [--monitoring] [--prod]
5
+ #
6
+ # Commands:
7
+ # up Start all containers (default)
8
+ # down Stop all containers
9
+ # build (Re)build the API image
10
+ # logs Tail logs from all services
11
+ # api-logs Tail API logs only
12
+ # restart Restart all containers
13
+ # restart-api Restart only the API container
14
+ # dev Start deps in background + API in foreground (live logs)
15
+ # shell Open a shell inside the API container
16
+ # db psql into Postgres
17
+ # redis redis-cli into Redis
18
+ # migrate Apply database migrations (alembic upgrade head)
19
+ # makemigrations [msg] Generate a new migration from model changes
20
+ # ps Show container status
21
+ #
22
+ # Options:
23
+ # --monitoring Include the Prometheus/Grafana/Tempo/Loki stack
24
+ # --prod Production mode: no source bind-mount, serve the baked image
25
+ set -e
26
+
27
+ cd "$(dirname "$0")"
28
+
29
+ # Pick a compose command (v2 plugin preferred)
30
+ if docker compose version >/dev/null 2>&1; then
31
+ DC="docker compose"
32
+ elif command -v docker-compose >/dev/null 2>&1; then
33
+ DC="docker-compose"
34
+ else
35
+ echo "❌ Docker Compose not found. Install Docker Desktop or the compose plugin."
36
+ exit 1
37
+ fi
38
+
39
+ # Create .env from template on first run
40
+ if [ ! -f .env ]; then
41
+ echo "â„šī¸ No .env found — creating one from .env.template"
42
+ cp .env.template .env
43
+ fi
44
+
45
+ # Parse positional command + flags
46
+ MONITORING=false
47
+ PROD=false
48
+ POSITIONAL=()
49
+ for arg in "$@"; do
50
+ case "$arg" in
51
+ --monitoring) MONITORING=true ;;
52
+ --prod) PROD=true ;;
53
+ *) POSITIONAL+=("$arg") ;;
54
+ esac
55
+ done
56
+ COMMAND="${POSITIONAL[0]:-up}"
57
+
58
+ # Compose file composition
59
+ FILES="-f docker-compose.yml"
60
+ if [ "$PROD" = false ]; then
61
+ FILES="$FILES -f docker-compose.override.yml" # dev: bind-mount + reload
62
+ fi
63
+ if [ "$MONITORING" = true ]; then
64
+ FILES="$FILES -f docker-compose.monitoring.yml"
65
+ echo "🔍 Monitoring enabled — Grafana: http://localhost:3000 Prometheus: http://localhost:9090"
66
+ fi
67
+ DC="$DC $FILES"
68
+
69
+ case "$COMMAND" in
70
+ up)
71
+ echo "Starting shaapi..."
72
+ $DC up -d --build
73
+ echo "✅ API: http://localhost:8000 | Swagger: http://localhost:8000/admin/api/v1/docs"
74
+ ;;
75
+ down) $DC down ;;
76
+ build) $DC build ;;
77
+ logs) $DC logs -f ;;
78
+ api-logs) $DC logs -f api ;;
79
+ restart) $DC restart ;;
80
+ restart-api) $DC restart api ;;
81
+ dev)
82
+ $DC up -d postgres redis minio
83
+ $DC up --build api
84
+ ;;
85
+ shell) $DC exec api bash ;;
86
+ db) $DC exec postgres psql -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DATABASE:-shaapi}" ;;
87
+ redis) $DC exec redis redis-cli ;;
88
+ migrate) $DC exec api bash -c "cd backend && alembic upgrade head" ;;
89
+ makemigrations)
90
+ MSG="${POSITIONAL[1]:-auto}"
91
+ $DC exec api bash -c "cd backend && alembic revision --autogenerate -m '$MSG'"
92
+ ;;
93
+ ps) $DC ps ;;
94
+ *)
95
+ echo "Unknown command: $COMMAND"
96
+ echo "Run: up | down | build | logs | api-logs | restart | restart-api | dev | shell | db | redis | migrate | makemigrations | ps [--monitoring] [--prod]"
97
+ exit 1
98
+ ;;
99
+ esac