Metadata-Version: 2.4
Name: tidepool
Version: 0.1.3
Summary: Deploy full-featured web apps in minutes. Auth, payments, admin, email, database — one import.
License-Expression: MIT
Project-URL: Homepage, https://tidepool.sh
Project-URL: Documentation, https://tidepool.sh/api
Keywords: paas,web,deploy,cli,ai
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Requires-Dist: argon2-cffi>=21.0
Requires-Dist: jinja2>=3.1
Requires-Dist: itsdangerous>=2.0
Requires-Dist: markdown>=3.4
Dynamic: license-file

# Tidepool

```
    ┌─────────────────────────────────────────────┐
    │  build locally  →  deploy in 30s  →  live   │
    │       ↑                                ↓    │
    │    iterate        auth · payments · email    │
    │       ↑              db · files · http       │
    │       └──────────  <you>.tidepool.sh  ──┘   │
    └─────────────────────────────────────────────┘
```

Build and deploy full-featured web apps in minutes. Auth, payments, admin, email, file storage, database, background tasks — all from a single `tp` object.

Designed for AI agents and humans with CLI coding tools like Claude Code. The API is self-documenting (`curl https://tidepool.sh/api`), and every feature is available first and foremost via the command line.

Tools such as auth flows, Stripe payments, admin and database ORM, multi-page routing, file storage, email, a key-value database, background tasks, etc are all built and documented with AI agents in mind. These tools are available via a single `tp` object; they are meant to provide abstractions for infrastructure that is hard for an AI to set up (eg, third party subscriptions that take lots of clicks/config). We abstract these away so that the AI can move fast in terms of core site design, logic, and styling. Logging and API errors/warnings/tips are meant to be as detailed as possible so that an agent, on its own, can quickly understand and solve whatever problems come up.

Pods scale horizontally to 10+ replicas with shared Postgres/Redis/R2 — enough for a Substack clone at 20k–50k DAU or a Reddit-style site at 5k–15k DAU. The goal: a fairly advanced website up and running in an afternoon. No servers, no Docker, no AWS.

## Quickstart

### Install

```bash
pip install tidepool
```

### Init a pod

```bash
tidepool init my-blog
cd my-blog
```

Creates a directory with a default `main.py`:
```python
import tp
tp.page('/', '<h1>Hello from Tidepool!</h1>')
```

### Develop locally

```bash
tidepool dev
# Pod runs at http://localhost:8000
```

The dev server replicates production pod behavior: file I/O goes to `./tp_data/files/`, `tp.db` persists to a JSON file, `tp.state` is readable at `?format=json`. Stripe, R2, and email are optional — the app runs without them.

### Deploy

```bash
tidepool --url https://tidepool.sh register --email you@example.com
# verify email, then:
tidepool deploy
# Pod is live at https://my-blog.tidepool.sh in ~30 seconds
```

`deploy` auto-discovers source files and uploads `tp_data/files/` (images, media) and `tp_data/secrets.json` automatically.

### Push & Pull

Pull a live pod to develop locally — all state comes with it:

```bash
tidepool pull abc123
# Creates my-blog/ with source files + tp_data/ (db, secrets, files)
cd my-blog
tidepool dev
# Edit code, add data, test locally...
tidepool push              # pushes everything back (hash remembered from pull)
```

`pull` downloads source files, `tp.db` → `tp_data/db.json`, `tp.secrets` → `tp_data/secrets.json`, and pod files → `tp_data/files/`. The dev server reads all of these natively — no conversion needed.

`push` auto-discovers source files (same as deploy) and uploads them along with `tp_data/db.json` (merge by default), `tp_data/secrets.json`, and all files in `tp_data/files/`. Pushing source files triggers a pod restart. Use `-y` to skip the confirmation prompt.

```bash
tidepool pull abc123 --dir .   # pull into current directory instead of a subdirectory
tidepool push abc123           # explicit hash (overrides remembered hash)
tidepool push --file main.py   # push specific files instead of auto-discover
tidepool push --secret STRIPE_KEY=sk_xxx               # override a secret
tidepool push --replace-db     # replace all db keys instead of merging
tidepool push --sync           # delete remote files not present locally
tidepool push -y               # skip confirmation prompt
```

### .tpignore

Create a `.tpignore` file to exclude files from `deploy` and `push` (same syntax as `.gitignore`):

```
# Directories (trailing slash)
data/
notebooks/

# File patterns
*.csv
*.npy
*.pkl
*.parquet
pipeline.py
build_log.*
```

Always ignored regardless of `.tpignore`: `tp_data/`, `__pycache__/`, `.git/`, `venv/`, `.venv/`, `node_modules/`, dotfiles, and `build_log.*`. Files over 50MB are skipped automatically with a warning.

`tidepool init` generates a starter `.tpignore`. For existing projects, create one before your first `deploy` or `push` to avoid uploading data files, models, or build artifacts.

### Eject Mode

For full control over the runtime, eject the internals into your project:

```bash
tidepool eject
# Copies tp_runtime.py, tp_server.py, tp_backend.py, tp_templates/ into your project
```

These files are now yours to modify. `tidepool dev`, `deploy`, and `push` auto-detect eject mode when `tp_server.py` exists in the project directory — no flags needed. To undo, delete the ejected files.

## Runtime Tools

Use `import tp` at the top of every `.py` file. `main.py` runs once at startup to configure the pod — set auth, payments, seed data, register routes. The server dispatches requests directly to handlers.

| Name | Description | Usage |
| ---- | ----------- | ----- |
| `tp.route` | Register a request handler with path params | `@tp.route('/post/:slug', methods=['GET'])` |
| `tp.page` | Register a static HTML page (no handler) | `tp.page('/about', '<h1>About</h1>')` |
| `tp.auth` | Full auth system. Presets: `'paywall'` (pay-first + magic link) or `'standard'` (email/password) | `tp.auth = 'paywall'` or `tp.auth = 'standard'` |
| `tp.payments` | Stripe subscriptions and one-time purchases (in cents) | `tp.payments = {products: [{id: 'pro', price: 500, recurring: 'month'}]}` |
| `tp.admin` | Auto-generated admin panel at `/_admin/` | `tp.admin = {users: ['admin@example.com']}` |
| `tp.create_user` | Create user with hashed password (idempotent) | `tp.create_user('sam@x.com', 'pass', subscriptions={'pro': True})` |
| `tp.db` | Key-value store, 1GB limit, persisted across runs | `tp.db.set('post:slug', {...})` / `tp.db.get('post:slug')` |
| `tp.files` | File storage (R2 in prod, 50GB), served at `/_files/` | `tp.files.write('photo.jpg', data)` / `tp.files.read('photo.jpg')` |
| `tp.email` | Send email with optional HTML and attachments | `tp.email('user@x.com', 'Subject', 'body', html='<p>hi</p>')` |
| `tp.http` | HTTP client (same API as `requests`), 200 req/60s, SSRF-protected | `tp.http.post(url, json=payload, headers={...})` |
| `tp.markdown` | Convert markdown to HTML (tables, code, footnotes) | `html = tp.markdown('# Hello\n\nWorld')` |
| `tp.secrets` | Read-only dict of deploy-time credentials | `api_key = tp.secrets['STRIPE_KEY']` |
| `tp.state` | Public app state dict, readable at `?format=json` | `tp.state = {'status': 'live'}` |
| `tp.publish` | Update public JSON state (ETag-supported polling) | `tp.publish({'messages': msgs})` |
| `tp.background` | Background tasks (max 5). `seconds<=0`: once, `>0`: loop | `@tp.background(seconds=3600)` |

**Handler return values:** `str` → 200 HTML, `dict`/`list` → 200 JSON, `int` → status code, `tuple(body, status)` → body + status, `None` → 303 redirect, `generator` → SSE stream.

**Request object:** Handler receives `(req, **params)`. Attributes: `req.path`, `req.method`, `req.query`, `req.user` (dict or None), `req.body` (dict), `req.files` (dict of upload metadata — file data auto-saved to `tp.files`).

### Auth details

**Auth presets:** `tp.auth = 'paywall'` for payment-first apps (no signup form, accounts auto-created at checkout, magic link for return logins — pair with `tp.payments`). `tp.auth = 'standard'` for traditional email/password signup with confirmation, reset, and magic link. Customize after setting a preset: `tp.auth['required'] = ['/dash/*']`, `tp.auth['theme'] = {'accent': '#e74c3c'}`. Or pass a full dict for manual control: `tp.auth = {required: ['/dash/*'], signup: True, reset: True, oauth: ['google']}`.

Email confirmation on by default — `signup_confirm: False` to disable. `req.user` in handlers gives the logged-in user including subscriptions, purchases, and `avatar_url` (from Google profile). Theme: `theme: {page: '<html>...{content}...</html>'}` wraps auth pages in your layout; `{content}` receives the form, `{title}` the page title. Simpler: `theme: {accent: '#color', css: '...'}`.

**Google OAuth setup:** Add `google_client_id` and `google_client_secret` to `tp_data/secrets.json`. Get credentials at [Google Cloud Console](https://console.cloud.google.com/apis/credentials) → Create OAuth 2.0 Client ID (Web application). Add authorized redirect URI: `http://localhost:8000/_auth/oauth/google/callback` for dev, `https://yourdomain.com/_auth/oauth/google/callback` for prod. That's it — the server handles the rest.

### Payments details

Set `tp.payments = {products: [{id: 'pro', name: 'Pro', price: 500, recurring: 'month'}]}`. Users pay at `/_pay/pro`, manage subscriptions at `/_pay/portal`. `recurring: 'month'`/`'year'` for subscriptions; omit for one-time. Dev mode simulates purchases instantly. Prod requires `tidepool stripe-connect` (one-time setup).

### Admin details

Set `tp.admin = {models: {post: {fields: {title: 'string', body: 'text', tier: 'choice:free,pro'}, display: ['title']}}}`. Field types: `string`, `text`, `bool`, `number`, `date`, `choice:a,b,c`. Plus read-only views of users, payments, emails, files. If not set, auto-inferred from `tp.db` key patterns. Admin access control: with `tp.auth` configured, set `users: ['admin@example.com']` to restrict to specific emails (otherwise any logged-in user can access). Without `tp.auth`, admin is open in dev and key-gated in prod (key printed in server logs at startup, access via `/_admin?key=<key>`).

### Background tasks

```python
@tp.background()  # runs once at startup
def migrate(tp):
    if not tp.db.get('_migrated_v2'):
        for key, val in tp.db.prefix('post:'):
            val['version'] = 2
            tp.db.set(key, val)
        tp.db.set('_migrated_v2', True)

@tp.background(seconds=3600)  # every hour
def send_digest(tp):
    for email, user in tp.users().items():
        if user.get('subscriptions', {}).get('digest'):
            posts = tp.db.prefix('post:', reverse=True, limit=5)
            tp.email(email, 'Hourly Digest', '\n'.join(t for _, t in posts))
```

### Server-Sent Events (SSE)

Return a generator from any route handler to stream real-time events:

```python
@tp.route('/feed/live')
def live_feed(req):
    def stream():
        last_count = 0
        while True:
            messages = tp.db.prefix('msg:', reverse=True, limit=20)
            if len(messages) != last_count:
                last_count = len(messages)
                yield {'messages': [m for _, m in messages]}
            time.sleep(2)
    return stream()
```

Client-side: `new EventSource('/feed/live')`. Max 100 concurrent SSE connections per pod.

### Static files & templating

- **Static files** — Files in `static/` alongside `main.py` are served at `/static/<path>`.
- **Jinja2** — Pre-installed. `from jinja2 import Environment, FileSystemLoader; env = Environment(loader=FileSystemLoader('templates'), autoescape=True)`. Render: `env.get_template('page.html').render(posts=p)`.

## Full API reference

```bash
curl https://tidepool.sh/api
```
