Add EvoSync v3 environment scaffold
FastAPI + Celery + Redis + MariaDB stack with 6-service docker-compose. Includes project skeleton (config, database, models, tasks, migrations) and health endpoint with passing test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
web/__init__.py
Normal file
0
web/__init__.py
Normal file
31
web/config.py
Normal file
31
web/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DATABASE_URL: str = "mysql+pymysql://evosync:evosync@db:3306/evosync"
|
||||
REDIS_URL: str = "redis://redis:6379/0"
|
||||
SECRET_KEY: str = "change-me-in-production"
|
||||
BASE_URL: str = "http://localhost:8000"
|
||||
PASSWORD_RESET_EXPIRE_MINUTES: int = 60
|
||||
|
||||
EVOTOR_APP_ID: str = ""
|
||||
EVOTOR_WEBHOOK_SECRET: str = ""
|
||||
|
||||
JIVOSITE_WIDGET_ID: str = ""
|
||||
VK_DEFAULT_PHOTO_PATH: str = "/app/default_product.png"
|
||||
VK_API_VERSION: str = "5.199"
|
||||
|
||||
CATALOG_REFRESH_INTERVAL_SECONDS: int = 3600
|
||||
|
||||
FLOWER_USER: str = "admin"
|
||||
FLOWER_PASSWORD: str = "changeme"
|
||||
|
||||
DB_ROOT_PASSWORD: str = ""
|
||||
DB_NAME: str = ""
|
||||
DB_USER: str = ""
|
||||
DB_PASSWORD: str = ""
|
||||
|
||||
model_config = {"env_file": ".env", "case_sensitive": False, "extra": "ignore"}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
24
web/database.py
Normal file
24
web/database.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
|
||||
from web.config import settings
|
||||
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600,
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
19
web/main.py
Normal file
19
web/main.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import logging
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
try:
|
||||
from pythonjsonlogger import jsonlogger
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(jsonlogger.JsonFormatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
|
||||
logging.root.addHandler(handler)
|
||||
except ImportError:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.root.setLevel(logging.INFO)
|
||||
|
||||
app = FastAPI(title="EvoSync")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
50
web/migrations/env.py
Normal file
50
web/migrations/env.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from web.database import Base
|
||||
import web.models # noqa: F401 — ensure all models are imported before autogenerate
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
from web.config import settings
|
||||
|
||||
configuration = config.get_section(config.config_ini_section, {})
|
||||
configuration["sqlalchemy.url"] = settings.DATABASE_URL
|
||||
|
||||
connectable = engine_from_config(
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
25
web/migrations/script.py.mako
Normal file
25
web/migrations/script.py.mako
Normal file
@@ -0,0 +1,25 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
21
web/migrations/versions/0001_initial.py
Normal file
21
web/migrations/versions/0001_initial.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""initial
|
||||
|
||||
Revision ID: 0001
|
||||
Revises:
|
||||
Create Date: 2026-04-27
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
revision: str = "0001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
0
web/models/__init__.py
Normal file
0
web/models/__init__.py
Normal file
0
web/tasks/__init__.py
Normal file
0
web/tasks/__init__.py
Normal file
22
web/tasks/celery_app.py
Normal file
22
web/tasks/celery_app.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from celery import Celery
|
||||
|
||||
from web.config import settings
|
||||
|
||||
celery_app = Celery("evosync", broker=settings.REDIS_URL, backend=settings.REDIS_URL)
|
||||
|
||||
celery_app.conf.update(
|
||||
task_serializer="json",
|
||||
result_serializer="json",
|
||||
accept_content=["json"],
|
||||
timezone="Europe/Moscow",
|
||||
enable_utc=True,
|
||||
task_track_started=True,
|
||||
task_acks_late=True,
|
||||
worker_prefetch_multiplier=1,
|
||||
broker_connection_retry_on_startup=True,
|
||||
task_routes={
|
||||
"web.tasks.sync.*": {"queue": "sync"},
|
||||
"web.tasks.health.*": {"queue": "health"},
|
||||
"web.tasks.catalog.*": {"queue": "default"},
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user