Compare commits

..

10 Commits

Author SHA1 Message Date
b2f65ba21f chore: update the patch version to 0.0.2 2025-04-07 16:33:57 +05:00
45effe5a33 Merge pull request 'VORKOUT-10' (#4) from VORKOUT-10 into master
Reviewed-on: #4
Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
Reviewed-by: ivan.dev <ivan.dev@heado.ru>
2025-04-07 16:32:36 +05:00
83b723f5b9 feat: add init project to Makefile 2025-04-03 12:26:54 +05:00
8b62b8b7a6 refactor(docker-compose): rename containers to prevent duplicate names 2025-04-03 11:12:28 +05:00
ff265ce1d4 Merge pull request 'feat VORKOUT-3: added sql tables, db connection, schemas' (#2) from VORKOUT-3 into master
Reviewed-on: #2
Reviewed-by: Vladislav Syrochkin <vlad.dev@heado.ru>
Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
2025-04-02 11:55:32 +05:00
TheNoxium
c9c5837b38 fix: config, list events table 2025-04-01 14:20:40 +05:00
TheNoxium
d7bef1ad26 feat: adder unsigned integer for id, function set expiry, "ps_id" 2025-03-31 12:34:41 +05:00
7b9ce61733 Merge pull request 'VORKOUT-2' (#3) from VORKOUT-2 into master
Reviewed-on: #3
2025-03-28 18:42:39 +05:00
TheNoxium
b95b7caefd feat: added sql tables and db connection 2025-03-28 17:32:57 +05:00
49be3c376c Merge pull request 'VORKOUT-2' (#1) from VORKOUT-2 into master
Reviewed-on: #1
2025-03-20 12:34:13 +05:00
23 changed files with 521 additions and 5 deletions

2
.gitignore vendored
View File

@ -1,6 +1,8 @@
venv/ venv/
node_modules/ node_modules/
init.lock
.idea/ .idea/
.vscode/ .vscode/
*.swp *.swp

View File

@ -37,5 +37,14 @@ revision:
cd $(API_APPLICATION_NAME)/db && \ cd $(API_APPLICATION_NAME)/db && \
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate
venv-api:
cd api && \
poetry install
install:
make migrate head && \
cd api && \
poetry run python3 api/utils/init.py
%:: %::
echo $(MESSAGE) echo $(MESSAGE)

View File

@ -1,6 +1,7 @@
import uuid import uuid
from os import environ from os import environ
from functools import cached_property
from pydantic import BaseModel from pydantic import BaseModel
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -43,6 +44,26 @@ class DefaultSettings(BaseSettings):
REDIS_DB: int = int(environ.get("REDIS_DB", "0")) REDIS_DB: int = int(environ.get("REDIS_DB", "0"))
REDIS_PASSWORD: str = environ.get("REDIS_PASSWORD", "hackme") REDIS_PASSWORD: str = environ.get("REDIS_PASSWORD", "hackme")
@cached_property
def database_settings(self) -> dict:
"""Get all settings for connection with database."""
return {
"database": self.MYSQL_DB,
"user": self.MYSQL_USER,
"password": self.MYSQL_PASSWORD,
"host": self.MYSQL_HOST,
"port": self.MYSQL_PORT,
}
@cached_property
def database_uri(self) -> str:
"""Get uri for connection with database."""
uri = "mysql+aiomysql://{user}:{password}@{host}:{port}/{database}".format(
**self.database_settings,
)
print("database_uri", uri)
return uri
class Config: class Config:
# env_file = "../.env" # env_file = "../.env"
env_file_encoding = "utf-8" env_file_encoding = "utf-8"

View File

@ -7,7 +7,7 @@ from sqlalchemy import pool
from alembic import context from alembic import context
from api.db import metadata from api.db import metadata, tables
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.

View File

@ -0,0 +1,63 @@
import contextlib
import json
import os
from typing import Any, AsyncGenerator
import asyncio
import sqlalchemy
from loguru import logger
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from sqlalchemy import URL,create_engine, text
from api.config import get_settings
from api.config.default import DbCredentialsSchema
class SessionManager:
engines: Any
def __init__(self, database_uri=get_settings().database_uri) -> None:
self.database_uri = database_uri
self.refresh(database_uri)
# self.reflect()
def __new__(cls, database_uri=get_settings().database_uri):
if not hasattr(cls, "instance"):
cls.instance = super(SessionManager, cls).__new__(cls)
cls.instance.engines = {}
return cls.instance
def refresh(self, database_uri) -> None:
# if not self.engines:
# self.engines = {}
if database_uri not in self.engines:
self.engines[database_uri] = create_async_engine(
database_uri,
echo=True,
future=True,
# json_serializer=serializer,
pool_recycle=1800,
pool_size=get_settings().CONNECTION_POOL_SIZE,
max_overflow=get_settings().CONNECTION_OVERFLOW,
)
def get_engine_by_db_uri(self, database_uri) -> AsyncEngine:
return self.engines[database_uri]
@contextlib.asynccontextmanager
async def get_connection(
database_uri=None,
) -> AsyncGenerator[AsyncConnection, None]:
if not database_uri:
database_uri = get_settings().database_uri
engine = SessionManager(database_uri).get_engine_by_db_uri(database_uri)
logger.debug(f"engine {engine} {SessionManager(database_uri).engines}")
async with engine.connect() as conn:
yield conn
async def get_connection_dep() -> AsyncConnection:
async with get_connection() as conn:
yield conn

18
api/api/db/sql_types.py Normal file
View File

@ -0,0 +1,18 @@
__all__ = ["BigIntegerPK", "SAEnum", "UnsignedInt"]
from typing import Any
from sqlalchemy import BigInteger, Enum, Integer
from sqlalchemy.dialects import mysql
# class SAEnum(Enum):
# def __init__(self, *enums: object, **kw: Any):
# validate_strings = kw.pop("validate_strings", True)
# super().__init__(*enums, **kw, validate_strings=validate_strings)
# # https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer
# BigIntegerPK = BigInteger().with_variant(Integer, "sqlite")
UnsignedInt = Integer().with_variant(mysql.INTEGER(unsigned=True), "mysql")

View File

@ -0,0 +1 @@
from . import account,events,process

View File

@ -0,0 +1,73 @@
from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy.sql import func
from datetime import datetime, timedelta
from enum import Enum, auto
from api.db.sql_types import UnsignedInt
from api.db import metadata
class AccountRole(str,Enum):
OWNER = auto()
ADMIN = auto()
EDITOR = auto()
VIEWER = auto()
class AccountStatus(str,Enum):
ACTIVE = auto()
DISABLED = auto()
BLOCKED = auto()
DELETED = auto()
account_table = Table(
'account', metadata,
Column('id', UnsignedInt, primary_key=True, autoincrement=True),
Column('name', String(100), nullable=False),
Column('login', String(100), nullable=False),
Column('email', String(100), nullable=True),
Column('bind_tenant_id', String(40), nullable=True),
Column('role', SQLAEnum(AccountRole), nullable=False),
Column('meta', JSON, default={}),
Column('creator_id', UnsignedInt, ForeignKey('account.id'), nullable=True),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('status', SQLAEnum(AccountStatus), nullable=False),
Index('idx_login', 'login'),
Index('idx_name', 'name'),
)
class KeyType(str,Enum):
PASSWORD = auto()
ACCESS_TOKEN = auto()
REFRESH_TOKEN = auto()
API_KEY = auto()
class KeyStatus(str,Enum):
ACTIVE = auto()
EXPIRED = auto()
DELETED = auto()
account_keyring_table = Table(
'account_keyring', metadata,
Column('owner_id', UnsignedInt, ForeignKey('account.id'), primary_key=True, nullable=False),
Column('key_type', SQLAEnum(KeyType), primary_key=True, nullable=False),
Column('key_id', String(40), default=None),
Column('key_value', String(64), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('expiry', DateTime(timezone=True), nullable=True),
Column('status', SQLAEnum(KeyStatus), nullable=False), )
def set_expiry_for_key_type(key_type: KeyType) -> datetime:
match key_type:
case KeyType.ACCESS_TOKEN:
return datetime.now() + timedelta(hours=1) # 1 hour
case KeyType.REFRESH_TOKEN:
return datetime.now() + timedelta(days=365) # 1 year
case KeyType.API_KEY:
return datetime.max # max datetime

View File

@ -0,0 +1,29 @@
from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy.sql import func
from enum import Enum, auto
from api.db.sql_types import UnsignedInt
from api.db import metadata
class EventState(str, Enum):
AUTO = auto()
DESCRIPTED = auto()
class EventStatus(str, Enum):
ACTIVE = auto()
DISABLED = auto()
DELETED = auto()
list_events_table = Table(
'list_events', metadata,
Column('id', UnsignedInt, primary_key=True, autoincrement=True),
Column('name', String(40, collation='latin1_bin'), nullable=False,unique=True),
Column('title', String(64), nullable=False),
Column('creator_id', UnsignedInt, ForeignKey('account.id'), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('schema', JSON, default={}),
Column('state', SQLAEnum(EventState), nullable=False),
Column('status', SQLAEnum(EventStatus), nullable=False),
)

View File

@ -0,0 +1,84 @@
from sqlalchemy import Table, Column, Integer, String, Text, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index, PrimaryKeyConstraint
from sqlalchemy.sql import func
from enum import Enum, auto
from api.db.sql_types import UnsignedInt
from api.db import metadata
# Определение перечислений для статуса процесса
class ProcessStatus(str, Enum):
ACTIVE = auto()
STOPPING = auto()
STOPPED = auto()
DELETED = auto()
# Определение таблицы process_schema
process_schema_table = Table(
'process_schema', metadata,
Column('id', UnsignedInt, primary_key=True, autoincrement=True),
Column('title', String(100), nullable=False),
Column('description', Text, nullable=False),
Column('owner_id', UnsignedInt, ForeignKey('account.id'), nullable=False),
Column('creator_id', UnsignedInt, ForeignKey('account.id'), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('settings', JSON, default={}),
Column('status', SQLAEnum(ProcessStatus), nullable=False),
Index('idx_owner_id', 'owner_id',)
)
process_version_archive_table = Table(
'process_version_archive', metadata,
Column('id', UnsignedInt, autoincrement=True, nullable=False),
Column('ps_id', UnsignedInt, ForeignKey('process_schema.id'), nullable=False),
Column('version', UnsignedInt, default=1, nullable=False),
Column('snapshot', JSON, default={}),
Column('owner_id', UnsignedInt, ForeignKey('account.id'), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('is_last', UnsignedInt, default=0),
PrimaryKeyConstraint('id', 'version') )
class NodeStatus(str, Enum):
ACTIVE = auto()
DISABLED = auto()
DELETED = auto()
class NodeType(Enum):
TYPE1 = 'Type1'
TYPE2 = 'Type2'
TYPE3 = 'Type3'
ps_node_table = Table(
'ps_node', metadata,
Column('id', UnsignedInt, autoincrement=True, primary_key=True, nullable=False),
Column('ps_id', UnsignedInt, ForeignKey('process_schema.id'), nullable=False),
Column('node_type', SQLAEnum(NodeType), nullable=False),
Column('settings', JSON, default={}),
Column('creator_id', UnsignedInt, ForeignKey('account.id'), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('status', SQLAEnum(NodeStatus), nullable=False),
Index('idx_ps_id', 'ps_id')
)
class NodeLinkStatus(str, Enum):
ACTIVE = auto()
STOPPING = auto()
STOPPED = auto()
DELETED = auto()
node_link_table = Table(
'node_link', metadata,
Column('id', UnsignedInt, autoincrement=True, primary_key=True, nullable=False),
Column('link_name', String(20), nullable=False),
Column('node_id', UnsignedInt, ForeignKey('ps_node.id'), nullable=False),
Column('next_node_id', UnsignedInt, ForeignKey('ps_node.id'), nullable=False),
Column('settings', JSON, default={}),
Column('creator_id', UnsignedInt, ForeignKey('account.id'), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('status', SQLAEnum(NodeLinkStatus),nullable=False),
Index('idx_node_id', 'node_id'),
Index('idx_next_node_id', 'next_node_id'))

View File

View File

@ -0,0 +1,30 @@
import datetime
from enum import Enum
from pydantic import BaseModel, EmailStr, Field
class Role(Enum):
OWNER = "Owner"
ADMIN = "Admin"
EDITOR = "Editor"
VIEWER = "Viewer"
class Status(Enum):
ACTIVE = "Active"
DISABLED = "Disabled"
BLOCKED = "Blocked"
DELETED = "Deleted"
class User(BaseModel):
id: int
name: str = Field(..., max_length=100)
login: str = Field(..., max_length=100)
email: EmailStr = Field(..., max_length=100)
bind_tenant_id: str = Field(..., max_length=40)
role: Role
meta: dict
creator_id: int
is_active: bool
created_at: datetime
status: Status

View File

@ -0,0 +1,27 @@
import datetime
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
class Type(Enum):
PASSWORD = "password"
ACCESS_TOKEN = "access_token"
REFRESH_TOKEN = "refresh_token"
API_KEY = "api_key"
class Status(Enum):
ACTIVE = "Active"
EXPIRED = "Expired"
DELETED = "Deleted"
class AccountKeyring(BaseModel):
owner_id: int
key_type: Type
key_id: str = Field(..., max_length=40)
key_value: str = Field(..., max_length=64)
created_at: datetime
expiry: datetime
status: Status

View File

View File

@ -0,0 +1,25 @@
from pydantic import BaseModel, Field
from typing import Dict, Any
from datetime import datetime
from enum import Enum
class State(Enum):
AUTO = "Auto"
DESCRIPTED = "Descripted"
class Status(Enum):
ACTIVE = "Active"
DISABLED = "Disabled"
DELETED = "Deleted"
class ListEvent(BaseModel):
id: int
name: str = Field(..., max_length=40)
title: str = Field(..., max_length=64)
creator_id: int
created_at: datetime
schema: Dict[str, Any]
state: State
status: Status

View File

View File

@ -0,0 +1,20 @@
from pydantic import BaseModel, Field, conint
from typing import Dict, Any
from datetime import datetime
from enum import Enum
class Status(Enum):
ACTIVE = "Active"
STOPPING = "Stopping"
STOPPED = "Stopped"
DELETED = "Deleted"
class MyModel(BaseModel):
id: int
link_name: str = Field(..., max_length=20)
node_id: int
next_node_id: int
settings: Dict[str, Any]
creator_id: int
created_at: datetime
status: Status

View File

@ -0,0 +1,20 @@
from pydantic import BaseModel, Field
from typing import Dict, Any
from datetime import datetime
from enum import Enum
class Status(Enum):
ACTIVE = "Active"
STOPPING = "Stopping"
STOPPED = "Stopped"
DELETED = "Deleted"
class ProcessSchema(BaseModel):
id: int
title: str = Field(..., max_length=100)
description: str
owner_id: int
creator_id: int
created_at: datetime
settings: Dict[str, Any]
status: Status

View File

@ -0,0 +1,11 @@
from pydantic import BaseModel, Field
from typing import Dict, Any
from datetime import datetime
class ProcessStatusSchema(BaseModel):
id: int
version: int
snapshot: Dict[str, Any]
owner_id: int
created_at: datetime
is_last: int

View File

@ -0,0 +1,23 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Dict, Any
from enum import Enum
class NodeType(Enum):
pass
class Status(Enum):
ACTIVE = "Active"
DISABLED = "Disabled"
DELETED = "Deleted"
class Ps_Node(BaseModel):
id: int
ps_id: int
node_type: NodeType
settings: dict
creator_id: Dict[str, Any]
created_at: datetime
status: Status

60
api/api/utils/init.py Normal file
View File

@ -0,0 +1,60 @@
import os
import asyncio
import hashlib
import secrets
from api.db.connection.session import get_connection
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
INIT_LOCK_FILE = "../init.lock"
DEFAULT_LOGIN = "vorkout"
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
def generate_password() -> str:
return secrets.token_urlsafe(20)
async def init():
if os.path.exists(INIT_LOCK_FILE):
print("Sorry, service is already initialized")
return
async with get_connection() as conn:
password = generate_password()
hashed_password = hash_password(password)
create_user_query = account_table.insert().values(
name=DEFAULT_LOGIN,
login=DEFAULT_LOGIN,
role=AccountRole.OWNER,
)
res = await conn.execute(create_user_query)
user_id = res.lastrowid
create_key_query = account_keyring_table.insert().values(
owner_id=user_id,
key_type=KeyType.PASSWORD,
key_value=hashed_password,
status=KeyStatus.ACTIVE,
)
await conn.execute(create_key_query)
await conn.commit()
await conn.close()
with open(INIT_LOCK_FILE, "w") as lock_file:
lock_file.write("initialized\n")
print(f"Login: {DEFAULT_LOGIN}")
print(f"Password: {password}")
if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(init())

View File

@ -1,6 +1,6 @@
[project] [project]
name = "api" name = "api"
version = "0.0.1" version = "0.0.2"
description = "" description = ""
authors = [ authors = [
{name = "Vladislav",email = "vlad.dev@heado.ru"} {name = "Vladislav",email = "vlad.dev@heado.ru"}

View File

@ -3,13 +3,13 @@ version: "3.1"
services: services:
rabbitmq: rabbitmq:
image: rabbitmq:4-management-alpine image: rabbitmq:4-management-alpine
container_name: rabbitmq container_name: rabbitmq-connect
ports: ports:
- 5672:5672 - 5672:5672
- 15672:15672 - 15672:15672
db: db:
image: mysql:8.0 image: mysql:8.0
container_name: mysql container_name: mysql-connect
environment: environment:
MYSQL_ROOT_PASSWORD: hackme MYSQL_ROOT_PASSWORD: hackme
MYSQL_DATABASE: connect_test MYSQL_DATABASE: connect_test
@ -21,7 +21,7 @@ services:
- "3306:3306" - "3306:3306"
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: redis container_name: redis-connect
command: redis-server --requirepass password command: redis-server --requirepass password
environment: environment:
REDIS_PASSWORD: hackme REDIS_PASSWORD: hackme