From bfdc3e0f30ce938e07b72cce6a662aca99e5e49f Mon Sep 17 00:00:00 2001 From: Andriy Yevtushyn <129842532+Avionic23@users.noreply.github.com> Date: Tue, 12 May 2026 16:30:21 +0300 Subject: [PATCH 1/4] add dependency cycle detection and CI workflow --- .github/workflows/ci.yaml | 53 ++++++++++++++++++++++++++++ tests/test_cycles.py | 19 ++++++++++ tools/__init__.py | 0 tools/check_cycles.py | 73 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 tests/test_cycles.py create mode 100644 tools/__init__.py create mode 100644 tools/check_cycles.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..aced846 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,53 @@ +name: CI + +on: + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + + services: + db: + image: postgres:15-alpine + env: + POSTGRES_DB: expedition_db + POSTGRES_USER: expedition_user + POSTGRES_PASSWORD: expedition_pass + options: >- + --health-cmd "pg_isready -U expedition_user -d expedition_db" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgres://expedition_user:expedition_pass@localhost:5432/expedition_db + REDIS_URL: redis://localhost:6379 + SECRET_KEY: test-secret-key + DEBUG: "True" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run migrations + run: python manage.py migrate --noinput + + - name: Run tests + run: pytest tests/ diff --git a/tests/test_cycles.py b/tests/test_cycles.py new file mode 100644 index 0000000..0c4d0f9 --- /dev/null +++ b/tests/test_cycles.py @@ -0,0 +1,19 @@ +import pytest +import os +from tools.check_cycles import CycleDetector + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +class TestDependencyCycles: + @pytest.fixture(autouse=True) + def setup(self): + self.cycle_detector = CycleDetector(PROJECT_ROOT) + + def test_no_cycles_exist(self): + self.cycle_detector.add_directory('/expeditions/') + self.cycle_detector.add_directory('/authentication/') + cycles = self.cycle_detector.detect() + assert len(cycles) == 0, ( + f"Circular dependencies detected:\n" + + "\n".join(" -> ".join(c) for c in cycles) + ) \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/check_cycles.py b/tools/check_cycles.py new file mode 100644 index 0000000..6b2d52f --- /dev/null +++ b/tools/check_cycles.py @@ -0,0 +1,73 @@ +import ast +import os +from collections import defaultdict + + +class CycleDetector: + def __init__(self, root): + self.root = os.path.abspath(root) + self.dirs = set() + + def add_directory(self, directory): + self.dirs.add(self.root+directory) + + @staticmethod + def find_imports(filepath): + with open(filepath, 'r') as f: + tree = ast.parse(f.read()) + + imports = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + imports.append(alias.name) + elif isinstance(node, ast.ImportFrom): + if node.module: + imports.append(node.module) + return imports + + @staticmethod + def detect_cycles(graph): + visited = set() + in_stack = set() + cycles = [] + + def dfs(node, path): + visited.add(node) + in_stack.add(node) + path.append(node) + + for neighbor in graph.get(node, []): + if neighbor not in visited: + dfs(neighbor, path) + elif neighbor in in_stack: + cycle_start = path.index(neighbor) + cycles.append(path[cycle_start:] + [neighbor]) + + path.pop() + in_stack.remove(node) + + for node in graph: + if node not in visited: + dfs(node, []) + + return cycles + + def build_dependency_graph(self, project_path): + graph = defaultdict(set) + + for root, dirs, files in os.walk(project_path): + for f in files: + if f.endswith('.py'): + filepath = os.path.join(root, f) + module = filepath.replace(self.root, '').replace('/', '.').replace('.py', '').lstrip('.') + imports = self.find_imports(filepath) + for imp in imports: + graph[module].add(imp) + return graph + + def detect(self): + graph = defaultdict(set) + for directory in self.dirs: + graph.update(self.build_dependency_graph(directory)) + return self.detect_cycles(graph) \ No newline at end of file From b22ea7789a7e87c60430bc704d2d3fb42ac81b77 Mon Sep 17 00:00:00 2001 From: Andriy Yevtushyn <129842532+Avionic23@users.noreply.github.com> Date: Tue, 12 May 2026 16:42:22 +0300 Subject: [PATCH 2/4] ci: add ports mapping for docker containers --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aced846..f7b518c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,8 @@ jobs: POSTGRES_DB: expedition_db POSTGRES_USER: expedition_user POSTGRES_PASSWORD: expedition_pass + ports: + - 5432:5432 options: >- --health-cmd "pg_isready -U expedition_user -d expedition_db" --health-interval 5s @@ -23,6 +25,8 @@ jobs: redis: image: redis:7-alpine + ports: + - 6379:6379 options: >- --health-cmd "redis-cli ping" --health-interval 5s From 20df54e5146803e2c9e26966d19a92c9887b680f Mon Sep 17 00:00:00 2001 From: Andriy Yevtushyn <129842532+Avionic23@users.noreply.github.com> Date: Tue, 12 May 2026 16:59:49 +0300 Subject: [PATCH 3/4] auth: replace 403 with 401 in case of unauthorized user --- authentication/authentication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/authentication/authentication.py b/authentication/authentication.py index 5d1d6b2..0571dd6 100644 --- a/authentication/authentication.py +++ b/authentication/authentication.py @@ -8,6 +8,8 @@ from .models import Session, User class SessionAuthentication(BaseAuthentication): + def authenticate_header(self, request): + return 'Bearer' def authenticate(self, request: HttpRequest): header = request.headers.get("Authorization") if not header: From a6e296cf74c72d1396c152c4ea039596131ad3ad Mon Sep 17 00:00:00 2001 From: Andriy Yevtushyn <129842532+Avionic23@users.noreply.github.com> Date: Tue, 12 May 2026 20:30:26 +0300 Subject: [PATCH 4/4] refactor: apply encapsulation to improve modularity --- tools/check_cycles.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tools/check_cycles.py b/tools/check_cycles.py index 6b2d52f..28165b9 100644 --- a/tools/check_cycles.py +++ b/tools/check_cycles.py @@ -5,14 +5,14 @@ class CycleDetector: def __init__(self, root): - self.root = os.path.abspath(root) - self.dirs = set() + self._root = os.path.abspath(root) + self._dirs = set() def add_directory(self, directory): - self.dirs.add(self.root+directory) + self._dirs.add(self._root+directory) @staticmethod - def find_imports(filepath): + def _find_imports(filepath): with open(filepath, 'r') as f: tree = ast.parse(f.read()) @@ -27,7 +27,7 @@ def find_imports(filepath): return imports @staticmethod - def detect_cycles(graph): + def _detect_cycles(graph): visited = set() in_stack = set() cycles = [] @@ -53,21 +53,21 @@ def dfs(node, path): return cycles - def build_dependency_graph(self, project_path): + def _build_dependency_graph(self, project_path): graph = defaultdict(set) for root, dirs, files in os.walk(project_path): for f in files: if f.endswith('.py'): filepath = os.path.join(root, f) - module = filepath.replace(self.root, '').replace('/', '.').replace('.py', '').lstrip('.') - imports = self.find_imports(filepath) + module = filepath.replace(self._root, '').replace('/', '.').replace('.py', '').lstrip('.') + imports = self._find_imports(filepath) for imp in imports: graph[module].add(imp) return graph def detect(self): graph = defaultdict(set) - for directory in self.dirs: - graph.update(self.build_dependency_graph(directory)) - return self.detect_cycles(graph) \ No newline at end of file + for directory in self._dirs: + graph.update(self._build_dependency_graph(directory)) + return self._detect_cycles(graph) \ No newline at end of file