diff --git a/README.md b/README.md index e69de29..6f64a3e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,112 @@ +# Marvin + +A locally-hosted, autonomous AI agent with a persistent personality, continuous learning, and internet access. Built from scratch on Apple Silicon. + +--- + +## What is Marvin? + +Marvin is a locally-hosted AI agent I'm building from scratch. Not a chatbot, not a wrapper around an API — a full system with its own persistent personality, memory that survives reboots, the ability to search the internet, and (eventually) a physical robot body to live in. + +The core idea: everything runs on my own hardware. No cloud, no monthly bills, no data leaving my machine. Cloud is allowed only for two things: AI assistants helping me build Marvin (Claude, Claude Code as development tools), and Marvin reaching out to the internet when he needs to look something up (Tavily web search). The brain itself stays local, indefinitely, for free. + +I want Marvin to feel like a continuous person — not a stateless assistant that forgets every conversation. Same personality across sessions. Real opinions. Real preferences. Real memory of what we talked about last week. + +--- + +## Why I'm building this + +I'm a junior engineer with strong fundamentals and ambitious goals. After my first developer role, I wanted to build something serious from the ground up — using the same engineering practices a senior at a large company would use, but on a project I fully own. + +Marvin is that project. It forces me to learn the things juniors usually don't get exposed to: + +- **System architecture across multiple layers** — LLM serving, vector memory, scheduling, HTTP APIs. +- **Real version control discipline** — signed commits, PR-only workflow, branch protection rulesets, CI/CD with required status checks. +- **Database design with security priorities** — row-level security, encrypted columns, least-privilege roles. +- **Long-term planning broken into phases**, each with clear acceptance criteria. + +The goal isn't just to ship Marvin. It's to be able to walk into any technical interview and explain — in depth — every architectural decision I made and why. Every choice has an Architectural Decision Record (ADR). Every alternative I rejected is documented with reasons. If a senior engineer asks "why Postgres + pgvector and not a dedicated vector database?" — I can give the alternatives I considered and the trade-offs I accepted, in five minutes. + +--- + +## Architecture + +```text + ┌─────────────┐ + │ USER │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ FastAPI │ Phase 5 + └──────┬──────┘ + │ + ▼ + ┌─────────────────────┐ + │ LangGraph │ Phase 2+ + │ orchestrator │ + └──┬──────┬──────┬────┘ + │ │ │ + ┌────────────┘ │ └────────────┐ + ▼ ▼ ▼ + ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ + │ Ollama │ │ PostgreSQL + │ │ Tavily API │ + │ (LLM brain) │ │ pgvector │ │ (internet) │ + │ Phase 1 │ │ Phase 2 │ │ Phase 3 │ + └──────────────┘ └────────┬─────────┘ └─────────────┘ + │ + │ tasks / cache + ▼ + ┌────────────────┐ + │ APScheduler + │ Phase 4 + │ Redis │ + └────────────────┘ +``` + +The whole system has one outbound network dependency at runtime: Tavily, for web search. Everything else — the LLM, the database, the cache, the task queue — runs locally on the deployment machine. That's a deliberate choice for privacy, control, and cost. + +I'm building this in seven phases, not all at once. Phase 1 is just the brain (LLM serving + a Python wrapper). Each subsequent phase adds one layer: memory, internet, autonomy, server, personality, embodiment. + +--- + +## Tech stack + +| Layer | Tool | Status | Phase | +| ------------------------------ | ---------------------------------------- | -------- | ----- | +| Language | Python 3.12 via `uv` | locked | All | +| LLM serving | Ollama | locked | 1 | +| Starter model | LLaMA 3.1 8B | locked | 1 | +| Larger model evaluation | LLaMA 3.3 70B / Qwen 3 32B | target | 5 | +| HTTP client | httpx | locked | 1+ | +| Agent reasoning | LangGraph | locked | 2+ | +| Persistent memory (relational) | PostgreSQL 16+ | locked | 2 | +| Persistent memory (vectors) | pgvector | locked | 2 | +| Cache and task queue | Redis | deferred | 4+ | +| Internet search | Tavily API | locked | 3 | +| Scheduling | APScheduler | locked | 4 | +| HTTP and WebSocket server | FastAPI | locked | 5 | +| Version control | Git + GitHub | locked | All | +| CI/CD | GitHub Actions | locked | All | +| Lint and format | ruff, yamllint, markdownlint, actionlint | locked | All | +| Secret scanning | gitleaks (CI + pre-commit) | locked | All | + +Picking these tools wasn't random. Each one is documented in [`docs/PROJECT_MARVIN_SPEC.md`](docs/PROJECT_MARVIN_SPEC.md) with the alternatives I considered. Some highlights: + +- **`uv` over pip/pyenv:** modern dependency resolution, lockfile-based, dramatically faster. +- **`httpx` over `requests`:** sync today, async-ready for Phase 5 — same library, no rewrite needed when FastAPI comes in. +- **PostgreSQL + pgvector over a dedicated vector DB (Chroma, Qdrant, Weaviate):** one database, ACID guarantees, real SQL skills, single backup strategy. +- **SSH-signed commits over GPG:** same cryptographic property, far less setup ceremony. + +For full reasoning on every decision, see the spec. + +--- + +## More + +The full technical specification lives at [`docs/PROJECT_MARVIN_SPEC.md`](docs/PROJECT_MARVIN_SPEC.md). It covers: + +- The personality specification. +- All seven development phases in detail. +- Engineering practices (CI/CD, testing pyramid, conventional commits, branch protection rulesets). +- Security posture (threat model, controls, layered defenses). +- Architectural Decision Records for every major choice. diff --git a/docs/PROJECT_MARVIN_SPEC.md b/docs/PROJECT_MARVIN_SPEC.md index a908f71..472f1e3 100644 --- a/docs/PROJECT_MARVIN_SPEC.md +++ b/docs/PROJECT_MARVIN_SPEC.md @@ -2,7 +2,7 @@ > Living document. Lives in `docs/PROJECT_MARVIN_SPEC.md`. Updates via PR. -**Status:** Active  ·  **Updated:** 2026-05-18  ·  **Owner:** [@crisfmb](https://github.com/crisfmb)  ·  **Repo:** [crisfmb/marvin](https://github.com/crisfmb/marvin) +**Status:** Active  ·  **Updated:** 2026-05-21  ·  **Owner:** [@crisfmb](https://github.com/crisfmb)  ·  **Repo:** [crisfmb/marvin](https://github.com/crisfmb/marvin) --- @@ -10,13 +10,13 @@ Build **Marvin**: a locally-hosted, autonomous AI agent with a persistent personality, continuous learning, internet access, and human-like conversation. Long-term: place Marvin's brain into a physical robot body. -Marvin lives on Cris's hardware. No cloud dependencies for the core agent. Cloud is allowed for *help building* Marvin (Claude, Claude Code) and *outside knowledge* Marvin reaches for (Tavily web search). The brain itself runs on the M4 Max indefinitely, free of recurring cost. +Marvin runs locally. No cloud dependencies for the core brain. Cloud is allowed for two things only: helping build Marvin (Claude, Claude Code as dev tools), and outside knowledge Marvin reaches for at runtime (Tavily web search). The brain itself stays on-device indefinitely, with no recurring cost. --- ## 2. Personality Spec -Marvin is an idealised, trauma-free version of Cris: +Marvin's personality is engineered, not just prompted. Traits: - Confident, fiercely loyal to his creator. - Prioritises animals above humans. @@ -25,16 +25,23 @@ Marvin is an idealised, trauma-free version of Cris: - Honest; pushes back when wrong; admits uncertainty. - Curious; researches topics independently when permitted. -Personality is engineered, not just prompted: a long-running system prompt plus memory weighting plus reflection loops (Phase 6). +Personality emerges from three combined layers: a long-running system prompt, memory weighting (Phase 2), and reflection loops (Phase 6). Fine-tuning is on the long-term roadmap (Phase 5+) if prompt-and-memory engineering proves insufficient. --- -## 3. Hardware +## 3. Hardware Requirements -| Machine | Spec | Role | -| ------- | ---------------- | ----------------------------------------------------------------------------- | -| M4 Max | 16", 48 GB, 1 TB | Dedicated Marvin brain. Freshly formatted, separate macOS account. Runs 24/7. | -| M5 Max | 64 GB | Cris's personal/dev machine. Marvin work happens here, deploys to M4. | +Marvin targets Apple Silicon. The architecture is portable across Apple Silicon configurations. + +| Requirement | Minimum | Recommended | Notes | +| --- | --- | --- | --- | +| Unified memory (development) | 48 GB | 64 GB | 48 GB runs the 8B starter model plus the full memory layer comfortably. | +| Unified memory (70B evaluation) | 64 GB | 80 GB+ | Llama 3.3 70B at Q4_K_M quantization is ~42 GB; below 64 GB the runtime is OOM-prone. | +| Storage | 200 GB free | 500 GB free | Multiple model weights + Postgres data + logs. | +| OS | macOS (latest minus 1) | — | A dedicated user account is required for security isolation. | +| Network | Outbound HTTPS for Tavily | — | Everything else runs on `localhost`. | + +The production deployment runs 24/7 on a machine separate from the development machine, in a dedicated macOS account with FileVault enabled. --- @@ -45,7 +52,8 @@ Personality is engineered, not just prompted: a long-running system prompt plus | Language | Python 3.12 via `uv` | locked | All | | LLM serving | Ollama | locked | 1 | | Starter model | LLaMA 3.1 8B | locked | 1 | -| Target model | Qwen 3 32B | target | 1+ | +| Larger model evaluation | LLaMA 3.3 70B / Qwen 3 32B | target | 5 | +| HTTP client | httpx | locked | 1+ | | Agent reasoning | LangGraph | locked | 2+ | | Persistent memory (relational) | PostgreSQL 16+ | locked | 2 | | Persistent memory (vectors) | pgvector | locked | 2 | @@ -78,14 +86,14 @@ Marvin needs two kinds of persistent state: relational (conversation logs, sched **Alternatives considered:** -- **ChromaDB.** Embedded Python lib, easiest to start. Rejected: prototyping-grade, weak interview signal, migration becomes technical debt. -- **Redis with RediSearch.** Familiar from Shopify. Rejected as primary store: Redis's strength is cache, pub/sub, and queues, not durable vector storage. Reintroduced for that role in Phase 4. -- **Qdrant, Weaviate, Milvus.** Purpose-built vector DBs. Rejected: separate service, separate backups, separate failure modes. Single-store is cleaner for a one-dev project. +- **ChromaDB.** Embedded Python lib, easiest to start. Rejected: prototyping-grade, weak production signal, migration becomes technical debt. +- **Redis with RediSearch.** Rejected as primary store: Redis's strength is cache, pub/sub, and queues, not durable vector storage. Reintroduced for that role in Phase 4. +- **Qdrant, Weaviate, Milvus.** Purpose-built vector DBs. Rejected: separate service, separate backups, separate failure modes. Single-store is cleaner for a one-developer project. **Consequences:** - Single backup strategy, single connection pool, ACID guarantees on memories. -- Forces SQL and Postgres operations skill, pairing with cybersec masters curriculum (RLS, GRANT/REVOKE, encrypted columns, SSL/TLS, query planner). +- Forces real SQL and Postgres operations skill, with security priorities in scope (RLS, GRANT/REVOKE, encrypted columns, SSL/TLS, query planner). - Slight learning curve: local Postgres via Docker. ### ADR-002 — Redis as future cache and queue, not memory store @@ -100,7 +108,7 @@ Redis joins the stack in Phase 4 in two roles: short-term conversation cache (la Without commit signing, Git author metadata is just text. For a public repo that will eventually hold personal data and API tokens, that's a real impersonation threat. -**Decision:** Both machines (M4, M5) generate dedicated signing keys separate from authentication keys. Both public signing keys registered on GitHub as `Signing Key` type. The `protect main` ruleset has `Require signed commits` enabled. +**Decision:** Both development and production machines generate dedicated signing keys separate from authentication keys. Both public signing keys are registered on GitHub as `Signing Key` type. The `protect main` ruleset has `Require signed commits` enabled. **Alternatives considered:** @@ -113,230 +121,28 @@ Without commit signing, Git author metadata is just text. For a public repo that Branch governance uses GitHub Rulesets, not classic branch protection rules. Rulesets layer (multiple can apply to the same branch), support a three-state enforcement lifecycle (Disabled, Evaluate, Active), and are auditable by anyone with read access. ---- - -## 6. Architecture - -```text - ┌─────────────┐ - │ USER │ - └──────┬──────┘ - │ - ▼ - ┌─────────────┐ - │ FastAPI │ Phase 5 - └──────┬──────┘ - │ - ▼ - ┌─────────────────────┐ - │ LangGraph │ Phase 2+ - │ orchestrator │ - └──┬──────┬──────┬────┘ - │ │ │ - ┌────────────┘ │ └────────────┐ - ▼ ▼ ▼ - ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ - │ Ollama │ │ PostgreSQL + │ │ Tavily API │ - │ (LLM brain) │ │ pgvector │ │ (internet) │ - │ Phase 1 │ │ Phase 2 │ │ Phase 3 │ - └──────────────┘ └────────┬─────────┘ └─────────────┘ - │ - │ tasks / cache - ▼ - ┌────────────────┐ - │ APScheduler + │ Phase 4 - │ Redis │ - └────────────────┘ -``` - -Trust boundary: Marvin's M4. Only outbound network dependency at runtime is Tavily. Postgres, Ollama, and Redis run locally. - ---- - -## 7. Development Phases +### ADR-005 — httpx as the HTTP client across all phases -### Phase 0 — Foundations (in progress) +**Status:** Accepted, 2026-05-21 -- Hardware procured (M4, planned M5). -- Terminal stack: iTerm2, Powerlevel10k, uv, Python 3.12, Cursor, Ollama. -- GitHub repo with `.gitignore`. -- CI: lint, unit-test, secrets (PR #1 merged). -- Ruleset on `main`: PR required, status checks, conversation resolution, no force push, no deletion, no admin bypass. -- Signed commits configured on M4 (in progress). -- `Require signed commits` added to ruleset (pending key setup). -- `.env` and `python-dotenv` setup (Layer 2, pending). -- Pre-commit hooks with gitleaks and ruff (Layer 3, pending). +Marvin sends HTTP requests to Ollama's local API in Phase 1, to Tavily in Phase 3, and exposes an HTTP server via FastAPI in Phase 5. The HTTP library picked in Phase 1 sets the trajectory for the others. -### Phase 1 — Brain online +**Decision:** `httpx` for all HTTP work, starting in Phase 1. -- Ollama serving LLaMA 3.1 8B locally. -- Minimal Python wrapper (`marvin.brain`) that sends a prompt and returns a response. -- First end-to-end conversation loop in a CLI. -- Unit tests for the wrapper (mocked HTTP). -- Integration test against a real Ollama instance (local only). - -### Phase 2 — Memory layer - -- PostgreSQL 16+ running locally via Docker. -- pgvector extension enabled. -- Embedding model selection (candidate: BGE small/large via sentence-transformers). -- Schema: `conversations`, `messages`, `memories` (embedding column), `facts`. -- Memory storage and retrieval (basic RAG). -- LangGraph introduced: retrieval node, reasoning node, response node. - -### Phase 3 — Internet - -- Tavily API integration with rate limiting and result caching. -- Search results enter memory layer with provenance. -- Marvin decides when to search (LangGraph conditional node). - -### Phase 4 — Autonomy and scheduling - -- APScheduler for periodic tasks (memory consolidation, knowledge refresh, journal writes). -- Redis introduced: last-N-turns cache and job queue. -- Background reflection: Marvin summarises recent conversations into long-term memory. - -### Phase 5 — Server layer - -- FastAPI HTTP and WebSocket interface. -- Frontend decision parked (web vs CLI vs Mac native app). -- Token-based auth: only Cris's devices can talk to Marvin. - -### Phase 6 — Personality and continuous learning +**Alternatives considered:** -- Personality state machine: structured representation of Marvin's character. -- Memory consolidation patterns inspired by Letta (formerly MemGPT). -- Reflection loops: Marvin reasons about his own past conversations and updates beliefs. +- **`urllib` (standard library).** No install needed, always available. Rejected: verbose API, no native async, awkward for production-grade code. +- **`requests`.** Industry default for ~15 years. Familiar in every Python tutorial. Rejected: synchronous only, in maintenance mode since 2023. Choosing `requests` now would require rewriting every HTTP call once Phase 5 introduces async needs via FastAPI. +- **`aiohttp`.** Async-first HTTP library. Rejected: less ergonomic for the sync code in early phases; `httpx` covers both modes with one mental model and one set of docs. -### Phase 7 — Embodiment (future) +**Consequences:** -- Physical robot body. -- Sensors (camera, microphone) and actuators. -- Local-network deployment with secure pairing to brain. +- One HTTP library across all phases. Sync today, async-ready for Phase 5. +- Drop-in-compatible API with `requests` keeps the learning curve low. +- One mental model: one library, two modes (sync, async). --- -## 8. Folder Structure +## 6. Architecture ```text -marvin/ -├── .github/ -│ └── workflows/ -│ └── ci.yml -├── docs/ -│ ├── PROJECT_MARVIN_SPEC.md -│ ├── ADR/ -│ └── runbooks/ -├── src/ -│ └── marvin/ -│ ├── __init__.py -│ ├── version.py -│ ├── brain/ -│ ├── memory/ -│ ├── internet/ -│ ├── autonomy/ -│ ├── server/ -│ └── personality/ -├── tests/ -│ ├── unit/ -│ ├── integration/ -│ └── e2e/ -├── pyproject.toml -├── uv.lock -├── .yamllint -├── .markdownlint.yaml -├── .gitignore -└── README.md -``` - ---- - -## 9. Engineering Practices - -- **Style:** PEP 8 strict; type hints on every public function; docstrings (PEP 257) on every public function, class, and module. -- **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) — `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`. -- **Branches:** `type/short-description`, e.g., `feat/ollama-wrapper`, `chore/scaffold-and-ci`. -- **Pull requests:** Every change goes through a PR. No exceptions, including Cris's own work on his own repo. -- **Merge strategy:** "Create a merge commit" or "Rebase and merge" — both preserve history. No squash-merge. -- **CI:** Three jobs minimum — `lint`, `unit-test`, `secrets`. All required on `main`. -- **Testing pyramid:** Many unit tests, fewer integration, few E2E. Unit on every push; integration local and in CI; E2E local pre-release. -- **Documentation:** This spec plus ADRs in `docs/ADR/` plus module READMEs plus runbooks for ops procedures. - ---- - -## 10. Security Posture - -Marvin will eventually hold conversation history, learned preferences, and API tokens. Security is a first-class concern, woven through every phase rather than bolted on later. - -**Principles applied:** - -- **Least privilege.** Every credential, service account, and database role gets the minimum access needed. No god-mode tokens. -- **Defence in depth.** Multiple layers, each catching what others miss: `.gitignore`, gitleaks in CI, pre-commit gitleaks, signed commits, Postgres RLS (Phase 2+), encrypted columns for sensitive fields. -- **Key separation.** Auth keys are not signing keys are not deployment keys. Each compromise is contained. -- **Auditability.** Conversation logs are timestamped; signed commits prove authorship; rulesets are readable by anyone with repo access. - -### Controls - -| Layer | Control | Status | -| ----------------- | -------------------------------------------------- | ----------------- | -| Repo | `.gitignore` covering `.env`, secrets, model files | done | -| Repo | gitleaks scanning in CI | done | -| Repo | Pre-commit gitleaks (local) | pending (Layer 3) | -| Branch | Ruleset on `main`: PR required, no admin bypass | done | -| Branch | Required signed commits on `main` | in progress | -| Code | ruff, actionlint, yamllint, markdownlint in CI | done | -| Code | Dependabot for dependency CVEs | backlog | -| Code | CodeQL static analysis | backlog | -| Runtime | `.env` and `python-dotenv` for secrets | pending (Layer 2) | -| Runtime | All API keys rotated periodically | ongoing | -| DB (Phase 2) | Postgres SSL-only connections | planned | -| DB (Phase 2) | Row-Level Security on multi-user tables | planned | -| DB (Phase 2) | Encrypted columns for sensitive fields | planned | -| Network (Phase 5) | FastAPI bound to localhost by default | planned | -| Network (Phase 7) | Mutual TLS between robot body and brain | planned | - -### Threat model — running list - -- **GitHub PAT leaks.** Mitigated by gitleaks (CI plus pre-commit), `.gitignore`, signed commits preventing forged commits, branch protection preventing direct push to `main`. -- **M4 stolen.** Mitigated by FileVault on the dedicated account; sensitive Postgres fields encrypted; remote SSH disabled by default. -- **Ollama model compromised.** Mitigated: models pulled from official Ollama registry only; SHA verification by Ollama; isolated user account. -- **Tavily key leaks.** Mitigated: `.env` not in git; key rotatable; rate-limited usage caps blast radius. -- **Supply-chain attack via Python deps.** Mitigated: uv lockfile pinning; Dependabot alerts (planned). - ---- - -## 11. Success Criteria - -- Marvin runs 24/7 on M4 without intervention. -- Persistent memory survives reboots and long conversation gaps. -- Internet-search capability for facts beyond training data. -- Conversational coherence: feels like a continuous person, not stateless turns. -- Recognisable, consistent personality across sessions. -- Cris can explain every architectural decision in an interview. -- Eventual embodiment in a physical robot body. - ---- - -## 12. Glossary - -| Term | Definition | -| ------------- | ------------------------------------------------------------------------------------------------------------------------ | -| ADR | Architectural Decision Record. A short doc explaining *why* a design choice was made, including alternatives considered. | -| Agent | A program that wraps an LLM with tools and a reasoning loop, enabling autonomous decision-making. | -| Embedding | A vector representation of text used for semantic search. | -| HNSW | Hierarchical Navigable Small World. Graph-based index for fast nearest-neighbour search over vectors. | -| LLM | Large Language Model. Marvin's reasoning brain. | -| Local-first | Marvin runs on Cris's hardware, not in the cloud. Privacy, control, no ongoing cost. | -| pgvector | PostgreSQL extension adding a `vector` data type and nearest-neighbour search. | -| RAG | Retrieval-Augmented Generation. Pattern: embed, store, retrieve, inject into prompt, LLM answers. | -| Ruleset | GitHub's modern branch governance mechanism (successor to classic branch protection). | -| Signed commit | A commit cryptographically proven to come from a specific key holder. | -| Vector DB | Database optimised for nearest-neighbour search over high-dimensional vectors. | - ---- - -## 13. Changelog - -- **2026-05-18** — Cleaned markdown formatting for lint compliance. Same content as the 05-15 revision. -- **2026-05-15** — Locked pgvector as persistent memory (replaces ChromaDB). Locked SSH-signed commits required on `main`. Added Security Posture section. Added ADR section. Documented Redis future role. Switched branch governance language from "classic protection" to "Ruleset". -- **(prior)** — Initial spec (lost to M4 reformat): vision, personality, architecture, tech stack, hardware, seven phases, folder structure, engineering practices, success criteria. diff --git a/pyproject.toml b/pyproject.toml index 32a7b16..38766cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,9 @@ authors = [ { name = "Cristina Borges", email = "crisfmb@gmail.com" } ] requires-python = ">=3.12" -dependencies = [] +dependencies = [ + "httpx>=0.28.1", +] [build-system] requires = ["uv_build>=0.11.11,<0.12.0"] diff --git a/src/marvin/brain/__init__.py b/src/marvin/brain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/marvin/brain/hello.py b/src/marvin/brain/hello.py new file mode 100644 index 0000000..daa366e --- /dev/null +++ b/src/marvin/brain/hello.py @@ -0,0 +1,9 @@ +OLLAMA_URL = "http://localhost:11434/api/generate" +DEFAULT_MODEL = "llama3.1:8b" + + +def ask(prompt: str, model: str = DEFAULT_MODEL) -> str: + payload = {"model": DEFAULT_MODEL, "prompt": prompt, "stream": False} + print(payload) + + return f"Hello, {prompt}!" diff --git a/tests/unit/test_hello.py b/tests/unit/test_hello.py new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock index df2e091..849ad91 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,28 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -11,6 +33,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -24,6 +92,9 @@ wheels = [ name = "marvin" version = "0.1.0" source = { editable = "." } +dependencies = [ + { name = "httpx" }, +] [package.dev-dependencies] dev = [ @@ -31,6 +102,7 @@ dev = [ ] [package.metadata] +requires-dist = [{ name = "httpx", specifier = ">=0.28.1" }] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=9.0.3" }] @@ -77,3 +149,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]