Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion etc/docker-entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ run_service() {
export RUFF_CACHE_DIR=/tmp/.ruff
ruff check .
print_caption "Mypy check"
mypy .
mypy --cache-dir=/tmp/.mypy .
;;
*)
echo "APP_SERVICE environment variable is unexpected or was not provided (APP_SERVICE='${APP_SERVICE}')" >&2
Expand Down
34 changes: 17 additions & 17 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@ version = "0.1.0"
description = "Code Agent (some helper system)"
requires-python = ">=3.13,<3.14"
dependencies = [
"fastapi==0.122.0",
"fastapi==0.136.3",
"itsdangerous==2.2.0", # required by session engine
"uvicorn==0.38.0",
"pydantic==2.12.5",
"pydantic-settings==2.12.0",
"uvicorn==0.49.0",
"pydantic==2.13.4",
"pydantic-settings==2.14.1",
"httpx[socks]==0.28.1",
"sqlalchemy[asyncio]==2.0.44",
"sqladmin==0.22.0",
"alembic==1.17.2",
"sqlalchemy[asyncio]==2.0.50",
"sqladmin==0.27.1",
"alembic==1.18.4",
"asyncpg==0.31.0",
"pyjwt==2.10.1",
"pyjwt==2.13.0",
"pycryptodomex==3.23.0", # for API key encryptions
"uvloop==0.22.1",
]

[dependency-groups]
dev = [
"types-WTForms>=3.2,<4",
"pytest>=8.3,<9",
"pytest-asyncio>=1.2.0,<1.3", # were >=0.26.0,<0.27"
"coverage>=7.10,<7.11",
"black>=25.1,<26.0",
"ruff>=0.13,<0.15",
"mypy>=0.18,<1.19",
"types-wtforms==3.2.1.20260518",
"pytest==9.0.3",
"pytest-asyncio==1.4.0", # were >=0.26.0,<0.27"
"coverage==7.14.1",
"black==26.5.1",
"ruff==0.15.16",
"mypy==2.1.0",
# pycharm requires:
"setuptools>=78.1,<81.0",
"pip<26",
"setuptools==82.0.1",
"pip==26.1.2",
]

# === Ruff settings ===
Expand Down
1 change: 0 additions & 1 deletion src/migrations/versions/0003_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from alembic import op
import sqlalchemy as sa


revision: str = "0003"
down_revision: Union[str, None] = "0002"
branch_labels: Union[str, Sequence[str], None] = None
Expand Down
7 changes: 3 additions & 4 deletions src/modules/admin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

if TYPE_CHECKING:
from src.main import CodeAgentAPP
from src.db.models import BaseModel

ADMIN_VIEWS: tuple[type[BaseView], ...] = (
UserAdminView,
Expand Down Expand Up @@ -90,12 +89,12 @@ async def create(self, request: Request) -> Response:

return response

@staticmethod
def get_save_redirect_url(
self,
request: Request,
form: FormData,
model_view: ModelView,
obj: "BaseModel",
obj: Any,
) -> str | URL:
"""
Make more flexable getting redirect URL after saving model instance
Expand All @@ -108,7 +107,7 @@ def get_save_redirect_url(
# required for getting instance ID after base creation's method finished
redirect_url = str(obj.id)
else:
redirect_url = super().get_save_redirect_url(request, form, model_view, obj)
redirect_url = Admin.get_save_redirect_url(request, form, model_view, obj)

return redirect_url

Expand Down
1 change: 0 additions & 1 deletion src/modules/cli/simple_ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import sys
from typing import Any, Optional, ContextManager


DEFAULT_VENDOR_URL = "https://api.deepseek.com/v1"
DEFAULT_VENDOR = "code-agent"
DEFAULT_MODEL = "deepseek-chat"
Expand Down
16 changes: 14 additions & 2 deletions src/settings/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
LogLevelString = Annotated[
str, StringConstraints(to_upper=True, pattern=rf"^(?i:{LOG_LEVELS_PATTERN})$")
]
DEFAULT_LOG_FORMAT = "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)s] %(message)s"
JSON_LOG_FORMAT = (
'{"time":"%(asctime)s","level":"%(levelname)s","logger":"%(name)s",'
'"location":"%(filename)s:%(lineno)s","message":"%(message)s"}'
)


class LogDictConfig(TypedDict):
Expand Down Expand Up @@ -43,9 +48,16 @@ class LogSettings(BaseSettings):

level: LogLevelString = "INFO"
skip_static_access: bool = False
format: str = "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)s] %(message)s"
format: str = DEFAULT_LOG_FORMAT
datefmt: str = "%d.%m.%Y %H:%M:%S"

@property
def formatter_format(self) -> str:
if self.format.lower() == "json":
return JSON_LOG_FORMAT

return self.format

@property
def dict_config(self) -> LogDictConfig:
filters: list[logging.Filter] = []
Expand All @@ -57,7 +69,7 @@ def dict_config(self) -> LogDictConfig:
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": self.format,
"format": self.formatter_format,
"datefmt": self.datefmt,
},
},
Expand Down
1 change: 0 additions & 1 deletion src/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

from src.tests.mocks import MockAPIToken, MockUser, MockVendor, MockTestResponse, MockHTTPxClient


MINIMAL_ENV_VARS = {
"API_DOCS_ENABLED": "true",
"APP_SECRET_KEY": "test-key",
Expand Down
18 changes: 15 additions & 3 deletions src/tests/units/auth/test_auth_edge_cases.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import datetime

import jwt
import pytest
from unittest.mock import MagicMock
from pydantic import SecretStr
Expand All @@ -24,10 +26,9 @@ class TestTokenEdgeCases:
@pytest.mark.parametrize(
"secret_key",
[
pytest.param("", id="empty"),
pytest.param("a" * 1000, id="long"),
pytest.param("!@#$%^&*()_+-=[]{}|;:,.<>?`~", id="special-characters"),
pytest.param("секретный-ключ", id="unicode"),
pytest.param("__!@#$%^&*()_+-=[]{}|;:,.<>?`~__", id="special-characters"),
pytest.param("секретный-ключ-4MCfXpYyX1i5FRW5wkStW963MLgUzqsL", id="unicode"),
],
)
def test_token_with_various_secret_key(self, secret_key: str) -> None:
Expand All @@ -46,6 +47,17 @@ def test_token_with_various_secret_key(self, secret_key: str) -> None:
decoded = decode_api_token(generated.value, app_settings)
assert decoded.sub is not None

def test_token_with_empty_secret_key(self) -> None:
app_settings = AppSettings(
admin_username="test-username",
admin_password=SecretStr("test-password"),
app_secret_key=SecretStr(""),
vendor_encryption_key=SecretStr(""),
jwt_algorithm="HS256",
)
with pytest.raises(jwt.exceptions.InvalidKeyError, match="HMAC key must not be empty."):
make_api_token(expires_at=None, settings=app_settings)

def test_token_with_minimal_expiration(self, app_settings_test: AppSettings) -> None:
minimal_exp = utcnow(skip_tz=False) + datetime.timedelta(seconds=1)
generated = make_api_token(expires_at=minimal_exp, settings=app_settings_test)
Expand Down
12 changes: 11 additions & 1 deletion src/tests/units/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import SecretStr

from src.settings import AppSettings, get_app_settings
from src.settings.log import LOG_LEVELS_PATTERN, LogSettings
from src.settings.log import JSON_LOG_FORMAT, LOG_LEVELS_PATTERN, LogSettings

MINIMAL_ENV_VARS = {
"SECRET_KEY": "test-key",
Expand Down Expand Up @@ -73,6 +73,16 @@ def test_log_config(self) -> None:
for logger in ["src", "fastapi", "uvicorn.access", "uvicorn.error"]
)

def test_log_config_supports_json_format_alias(self) -> None:
settings = AppSettings(
app_secret_key=SecretStr("test-token"),
vendor_encryption_key=SecretStr("test-encryption-key"),
log=LogSettings(level="DEBUG", format="json"),
http_proxy_url=None,
)
log_config = settings.log.dict_config
assert log_config["formatters"]["standard"]["format"] == JSON_LOG_FORMAT


class TestGetSettings:
@patch.dict(
Expand Down
Loading
Loading