read.markets/pyproject.toml

52 lines
1.1 KiB
TOML
Raw Permalink Normal View History

[project]
name = "cassandra"
version = "0.1.0"
description = "Containerised macro-strategy dashboard — market data, news, portfolios, AI daily log."
requires-python = ">=3.13"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"jinja2>=3.1",
"python-multipart>=0.0.12",
"sqlalchemy[asyncio]>=2.0.36",
"aiomysql>=0.2.0",
"alembic>=1.14",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"httpx>=0.28",
"apscheduler>=3.10",
"tenacity>=9.0",
"structlog>=24.4",
phase A: user accounts + session-cookie auth Replaces the static bearer-token gate with a real auth boundary. The existing CASSANDRA_TOKEN path is retained as an admin / scripting escape hatch — kept compatible by aliasing require_token to require_auth. - New users table (migration 0007): email, argon2 password_hash, tier, email_verified (declared but not enforced until phase E), settings_json for the tone/analysis/anchor knobs we'll wire in phase D. - app/services/auth_service.py: argon2-cffi password hashing with timing- attack-resistant authenticate() (always runs a hash verify even on unknown-email to deny a username-enumeration oracle). - app/auth.py rewritten: require_auth returns a CurrentUser with either is_admin=True (bearer path) or a User object (session path). Failing requests get 303 → /login for HTML, 401 for API. Sessions signed with itsdangerous against CASSANDRA_SESSION_SECRET; 14-day TTL. - app/routers/auth.py: /login, /signup, /logout. Login form preserves the ?next=… param for redirect-after-login. Signup respects a new CASSANDRA_SIGNUP_ENABLED flag. - Standalone /login + /signup templates (no app chrome). base.html grows a user chip + logout link in the header (reads request.state.current_user). Phase A's main known limitations are documented in the plan: email verification is declared but not enforced; session revocation is best-effort (cookie-only, not DB-backed). Both land in phase E. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:12:10 +01:00
"argon2-cffi>=23.1",
"cryptography>=43.0",
phase A: user accounts + session-cookie auth Replaces the static bearer-token gate with a real auth boundary. The existing CASSANDRA_TOKEN path is retained as an admin / scripting escape hatch — kept compatible by aliasing require_token to require_auth. - New users table (migration 0007): email, argon2 password_hash, tier, email_verified (declared but not enforced until phase E), settings_json for the tone/analysis/anchor knobs we'll wire in phase D. - app/services/auth_service.py: argon2-cffi password hashing with timing- attack-resistant authenticate() (always runs a hash verify even on unknown-email to deny a username-enumeration oracle). - app/auth.py rewritten: require_auth returns a CurrentUser with either is_admin=True (bearer path) or a User object (session path). Failing requests get 303 → /login for HTML, 401 for API. Sessions signed with itsdangerous against CASSANDRA_SESSION_SECRET; 14-day TTL. - app/routers/auth.py: /login, /signup, /logout. Login form preserves the ?next=… param for redirect-after-login. Signup respects a new CASSANDRA_SIGNUP_ENABLED flag. - Standalone /login + /signup templates (no app chrome). base.html grows a user chip + logout link in the header (reads request.state.current_user). Phase A's main known limitations are documented in the plan: email verification is declared but not enforced; session revocation is best-effort (cookie-only, not DB-backed). Both land in phase E. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:12:10 +01:00
"itsdangerous>=2.2",
"email-validator>=2.2",
phase G: data minimisation + passwordless auth + DeepSeek-first LLM Server no longer holds portfolios. Holdings live in the browser (localStorage); the server publishes an anonymous ticker_universe and a gzipped /api/universe payload identical for every authenticated user, so access patterns can't betray which tickers a user holds. AI commentary is generated ephemerally from the browser-supplied pie and the cost ledger row records no positions. Migrations 0009-0011 added the universe table and dropped positions / portfolio_snapshots / portfolios. Authentication is now e-mail OTP only. Migration 0010 dropped password_hash and email_verified (every active session is by construction proof of email control). The /signup endpoint is gone; signup and login share a single email-entry page. Email rendering is HTML+plain-text multipart with a shared brand palette (app/branding.py) asserted in sync with the CSS by a drift-detection test. LLM provider defaults to DeepSeek-direct (cheaper, api.deepseek.com) with OpenRouter as automatic fallback if DeepSeek fails. ai_log_job and indicator_summary_job now iterate the two tones (NOVICE, INTERMEDIATE) per cycle so the dashboard's tone toggle is instant; PROMPT_VERSION bumped to 6 with an educational anti-TA / anti-gambling stance baked into _CORE. NOVICE mode renders a curated glossary inline (CBOE VIX, yield curve, HY OAS, etc.) with JS-positioned tooltips that survive viewport edges and sticky bars. Model name and tokens hidden from the user UI; still recorded in StrategicLog.model and AICall for admin. Layout adds a sticky top nav, a sticky bottom markets bar (one chip per exchange with status LED + headline index + 1d change), and Phase H feedback reporting is queued in tasks/todo.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:16:57 +01:00
"aiosmtplib>=3.0",
"redis[hiredis]>=5.2",
stripe: wire checkout, customer portal, and webhook for read.markets Stripe is the merchant-on-record for read.markets after Polar/Paddle both declined the financial-media category. This commit lands the full subscription flow: an "Upgrade" button on /pricing now opens a real Stripe-hosted Checkout, completes the subscription, and the webhook flips user.tier to "paid" idempotently. Endpoints - POST /api/stripe/checkout (require_auth) — creates a hosted Checkout Session in subscription mode, passes user.id as client_reference_id + email as customer_email, returns the URL for the page-side JS to redirect to. Reuses an existing stripe_customer_id to avoid duplicate Stripe customers on repeat checkouts. allow_promotion_codes=True so the referral-credit redemption can attach a coupon at checkout once that flow ships. - POST /api/stripe/portal (require_auth) — mints a Stripe Customer Portal session. Used by /settings; returns 404 until the user has a stripe_customer_id (i.e. completed at least one checkout). - POST /api/stripe/webhook — signature-verified via stripe.Webhook.construct_event. Idempotent via UNIQUE on stripe_events.event_id. Event dispatch: checkout.session.completed → grant paid, store IDs customer.subscription.created → grant paid (active/trialing) customer.subscription.updated → grant paid (active/trialing) customer.subscription.deleted → drop to free, clear sub id invoice.paid / failed → audit only charge.refunded → audit only Stripe-SDK objects don't expose dict.get(); we use the SDK for signature verification then re-parse the JSON body for handler dispatch — cleaner than reaching into StripeObject internals. Schema (migration 0019) - users.stripe_customer_id, users.stripe_subscription_id (nullable String(64), UNIQUE on customer_id). - stripe_events table mirroring polar_events: event_id (unique), event_type, received_at, processed_at, error, raw payload (truncated to 16 KiB). Settings (.env) - STRIPE_API_KEY (rk_test_… for dev, rk_live_… for GA) - STRIPE_WEBHOOK_SECRET (whsec_… from the dashboard endpoint) - STRIPE_PRICE_MONTHLY (price_xxx for £7/month) - STRIPE_PRICE_ANNUAL (price_xxx for £70/year) Pricing page - Free tier CTA unchanged. - Paid CTA branches three ways: paid → "Manage subscription" to /settings; logged-in free → two buttons (£7/mo, £70/yr) that POST to /api/stripe/checkout and redirect; anonymous → /login?next=/pricing. - Inline JS intercepts the button click, calls the checkout endpoint, redirects on success, surfaces errors via alert(). No Stripe.js dep — we use the hosted-checkout URL directly. Polar handler stays in place for berengar.io / flyroom.net which still ship through Polar. polar_* and stripe_* columns coexist independently on the User row. Tests - 9 in tests/test_stripe_billing.py covering: bad signature → 401, missing signature → 400, checkout.session.completed flips tier + stores IDs, subscription.updated active grants paid, subscription.deleted drops to free with customer id preserved, replayed event id is no-op (one row in stripe_events), unknown event acked 200, checkout endpoint mocks the SDK and returns the hosted URL, checkout requires login. - Full suite: 221 passed, 5 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:45:13 +02:00
"stripe>=11.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.24",
"pytest-httpx>=0.34",
"aiosqlite>=0.20",
"ruff>=0.7",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py313"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["app", "app.services", "app.jobs", "app.routers"]