This guide covers the technical architecture, request lifecycle, schema model, and extension rules for GraphSpy.
GraphSpy should continue to follow these rules:
Further reading: Wikipedia - Single-responsibility principle, Refactoring.Guru - Single Responsibility Principle
Keep each layer focused on one concern:
- src/graphspy/cli.py owns startup, argument parsing, and process wiring.
- src/graphspy/app.py owns Flask app composition and cross-cutting handlers.
- Files in src/graphspy/api own HTTP input/output handling.
- Files in src/graphspy/core own Microsoft protocol workflows, token logic, and business behavior.
- Files in src/graphspy/db own SQLite access, schema creation, and migration logic.
- src/graphspy/web/pages.py should remain route-to-template wiring, not a second business-logic layer.
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.
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.
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:
- src/graphspy/api/access_tokens.py is thin and delegates token persistence/decoding behavior.
- src/graphspy/core/requests_.py centralizes outbound request behavior, proxy handling, retry logic, and response shaping.
Further reading: Wikipedia - Don't repeat yourself, DevIQ - DRY Principle
Reuse the existing conventions and shared helpers:
- Use src/graphspy/logbook.py for logging setup and behavior.
- Use src/graphspy/core/errors.py for application-level errors that should flow through Flask handlers.
- Use src/graphspy/db/connection.py for request-scoped SQLite access.
- Use src/graphspy/api/helpers.py where a shared API response helper makes sense.
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.
GraphSpy has four primary layers.
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.
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
AppErrorinto 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.
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:
Core modules under src/graphspy/core implement the real M365 / Entra workflows. Representative modules:
- src/graphspy/core/tokens.py
- src/graphspy/core/device_codes.py
- src/graphspy/core/device.py
- src/graphspy/core/prt.py
- src/graphspy/core/winhello.py
- src/graphspy/core/mfa.py
- src/graphspy/core/teams.py
- src/graphspy/core/requests_.py
The data layer under src/graphspy/db owns:
- connection lifecycle through Flask
g - schema bootstrap
- schema migrations
- raw query/execute helpers
A normal request path looks like this:
- src/graphspy/cli.py starts the process and creates the app.
- src/graphspy/app.py registers blueprints and global handlers.
- A route in src/graphspy/api or src/graphspy/web/pages.py receives the request.
- API modules delegate to src/graphspy/core and/or src/graphspy/db.
- Shared DB access flows through src/graphspy/db/connection.py.
- Outbound Microsoft requests should generally flow through src/graphspy/core/requests_.py.
- 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.
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.
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.
Start in these files for common work:
- Access and refresh tokens:
- Device codes:
- Device registration and certificates:
- PRT and Windows Hello:
- MFA:
- Generic Graph/custom requests:
- Entra data retrieval:
- Teams:
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=Falseunless 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-Afterunless the task is specifically about throttling behavior. - Preserve user-agent conventions already used by the tool.
- 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
AppErrorfor controlled application-level failures that should return a clean JSON error.
- 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.
When service semantics matter, check Microsoft docs before changing behavior.
- Microsoft Graph overview: https://learn.microsoft.com/graph/overview
- Microsoft Graph REST API overview: https://learn.microsoft.com/graph/api/overview?view=graph-rest-1.0
- Microsoft Graph throttling guidance: https://learn.microsoft.com/graph/throttling
- Microsoft identity platform OAuth 2.0 device authorization grant: https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code
- Microsoft Graph auth concepts: https://learn.microsoft.com/graph/auth/auth-concepts
- Microsoft Graph user resource: https://learn.microsoft.com/graph/api/resources/user?view=graph-rest-1.0
- Microsoft Graph driveItem resource: https://learn.microsoft.com/graph/api/resources/driveitem?view=graph-rest-1.0
- Microsoft Graph site resource: https://learn.microsoft.com/graph/api/resources/site?view=graph-rest-1.0
- Microsoft Graph authentication methods overview: https://learn.microsoft.com/graph/api/resources/authenticationmethods-overview?view=graph-rest-1.0
- Microsoft Teams API overview: https://learn.microsoft.com/graph/api/resources/teams-api-overview?view=graph-rest-1.0
When using AI in any shape or form during development, please make sure to adhere to the AI policy!
A change is not complete until all are true:
- The change respects the CLI/app/API/core/DB boundaries described here.
- Schema changes update both fresh bootstrap and migration handling.
- Proxy, retry, and logging behavior remain intentional.
- Python 3.10+ compatibility is preserved.
- Relevant Microsoft API semantics were checked when behavior depends on them.
- pyproject.toml and uv.lock stay in sync when dependencies change.