Skip to content

Latest commit

 

History

History
259 lines (193 loc) · 13 KB

File metadata and controls

259 lines (193 loc) · 13 KB

Development Guide

This guide covers the technical architecture, request lifecycle, schema model, and extension rules for GraphSpy.

Design Principles

GraphSpy should continue to follow these rules:

Single Responsibility Principle (SRP)

Further reading: Wikipedia - Single-responsibility principle, Refactoring.Guru - Single Responsibility Principle

Keep each layer focused on one concern:

Open/Closed Principle (OCP)

Further reading: Wikipedia - Open-closed principle, Refactoring.Guru - Open Closed Principle

Extend behavior by adding focused modules or helper functions instead of rewriting shared plumbing:

  • New UI/API capability usually means a focused API module or endpoint plus a matching page/template when needed.
  • New outbound workflow logic belongs in the relevant file under src/graphspy/core.
  • Shared HTTP behavior should be extended through src/graphspy/core/requests_.py, not reimplemented ad hoc.
  • Schema changes must remain compatible with both fresh initialization and upgrade paths.

Composition over sprawl

Further reading: Wikipedia - Composition over inheritance, Refactoring.Guru - Favor Composition Over Inheritance

GraphSpy is mostly module-based composition rather than inheritance-heavy OOP:

  • src/graphspy/app.py composes the application by registering API and page blueprints.
  • API modules should compose helpers from src/graphspy/core and src/graphspy/db instead of embedding their own versions.
  • Keep helpers small and reusable so new features can be assembled from existing building blocks.

Thin API, thick core

API modules should translate HTTP requests into calls to core logic, then format the response. They should not own protocol semantics, token derivation, throttling strategy, or database design.

Representative examples:

DRY and consistent conventions

Further reading: Wikipedia - Don't repeat yourself, DevIQ - DRY Principle

Reuse the existing conventions and shared helpers:

Fail fast and observable behavior

GraphSpy is an operator tool. Hidden behavior changes are risky.

  • Prefer explicit errors over silent fallback.
  • Preserve operator-visible behavior unless the change explicitly targets it.
  • Keep logging consistent so requests, failures, and migrations remain debuggable.
  • Do not silently remove proxy support, retries, or intentional certificate-verification bypass under proxied operation.

Architecture

GraphSpy has four primary layers.

1. CLI and process startup

src/graphspy/cli.py is the entry point. It is responsible for:

  • parsing interface, port, database, proxy, debug, and dev flags
  • resolving the application directory and database path
  • bootstrapping the database on first use
  • creating the Flask app
  • launching Waitress in normal mode or the Flask development server in dev mode

This file should stay focused on process-level orchestration, not product feature logic.

2. Flask application composition

src/graphspy/app.py creates the Flask app and configures shared behavior.

Current responsibilities:

  • configure app paths and runtime settings
  • register all API blueprints and the page blueprint
  • log requests in after_request
  • translate AppError into JSON responses
  • close the SQLite connection on app-context teardown
  • suppress duplicate Werkzeug request logging

This file is the right place for app-wide middleware-style behavior, not feature-specific Microsoft logic.

3. API and web layer

API modules under src/graphspy/api expose HTTP endpoints. Typical responsibilities:

  • read form/query/path input
  • perform light validation
  • delegate to core or DB helpers
  • return JSON, redirects, or simple values

The page blueprint in src/graphspy/web/pages.py maps routes to templates and should remain intentionally thin.

Templates and assets live under:

4. Core workflows and data layer

Core modules under src/graphspy/core implement the real M365 / Entra workflows. Representative modules:

The data layer under src/graphspy/db owns:

  • connection lifecycle through Flask g
  • schema bootstrap
  • schema migrations
  • raw query/execute helpers

Request Lifecycle

A normal request path looks like this:

  1. src/graphspy/cli.py starts the process and creates the app.
  2. src/graphspy/app.py registers blueprints and global handlers.
  3. A route in src/graphspy/api or src/graphspy/web/pages.py receives the request.
  4. API modules delegate to src/graphspy/core and/or src/graphspy/db.
  5. Shared DB access flows through src/graphspy/db/connection.py.
  6. Outbound Microsoft requests should generally flow through src/graphspy/core/requests_.py.
  7. src/graphspy/app.py logs the response and tears down DB state.

If an application-level failure should be surfaced cleanly, raise src/graphspy/core/errors.py AppError so the global error handler can respond consistently.

Database Model

Schema bootstrap

src/graphspy/db/schema.py defines the current schema for a fresh database. It currently creates tables for:

  • access tokens
  • refresh tokens
  • device codes
  • request templates
  • Teams settings
  • MFA OTP secrets
  • device certificates
  • primary refresh tokens
  • Windows Hello keys
  • settings

Fresh bootstrap writes the current schema version to settings.

Migrations

src/graphspy/db/migrations.py upgrades older databases in-place by stepping through schema versions sequentially.

Rules:

  • Never update only the fresh schema and forget upgrades.
  • Never update only migrations and forget fresh installs.
  • Prefer additive migrations that preserve existing operator data.
  • Keep schema version changes explicit and linear.

If you add a table or column, update both files in the same change.

Feature Boundaries

Start in these files for common work:

Request, Proxy, and HTTP Rules

Outbound Microsoft requests should stay behaviorally consistent.

  • Prefer src/graphspy/core/requests_.py for outbound HTTP so headers, user-agent behavior, proxy settings, and retry logic stay centralized.
  • If a proxy is configured, the current code intentionally sets verify=False unless explicitly overridden. That is part of the operator workflow for this tool and should not be casually changed.
  • Preserve the existing retry behavior for HTTP 429 with Retry-After unless the task is specifically about throttling behavior.
  • Preserve user-agent conventions already used by the tool.

Logging and Error Handling

  • Logging is configured in src/graphspy/logbook.py with Loguru and stdlib interception.
  • Request logging is done centrally in src/graphspy/app.py.
  • Use explicit, operator-meaningful error messages.
  • Use logger.exception(...) when stack traces matter during exception handling.
  • Use AppError for controlled application-level failures that should return a clean JSON error.

Python Conventions

  • Python baseline is 3.10+.
  • Use modern typing syntax compatible with 3.10+.
  • Group imports with explicit section comments.
  • Prefer small focused helpers over large mixed-purpose functions.
  • Avoid broad refactors in unrelated files.
  • Keep diffs minimal and behavior-preserving unless a behavioral change is the point of the task.

Microsoft Documentation

When service semantics matter, check Microsoft docs before changing behavior.

AI Usage

When using AI in any shape or form during development, please make sure to adhere to the AI policy!

Definition of Done

A change is not complete until all are true:

  1. The change respects the CLI/app/API/core/DB boundaries described here.
  2. Schema changes update both fresh bootstrap and migration handling.
  3. Proxy, retry, and logging behavior remain intentional.
  4. Python 3.10+ compatibility is preserved.
  5. Relevant Microsoft API semantics were checked when behavior depends on them.
  6. pyproject.toml and uv.lock stay in sync when dependencies change.