Compare commits
10 Commits
d3c85acd1e
...
78e3c34d8d
| Author | SHA1 | Date | |
|---|---|---|---|
| 78e3c34d8d | |||
| 2e631136c6 | |||
| 613d8f28ea | |||
| e684ac2853 | |||
| 4fd19bf620 | |||
| e5ca1c0ada | |||
| 8fc81fc4f4 | |||
| 142a0dbff6 | |||
| da47923088 | |||
| 5e787edbb0 |
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# handbook
|
||||
|
||||
The Rethink Studios handbook — our public reference for the shared library suite,
|
||||
coding standards, the dev workflow, Python environments, and how to deploy on our
|
||||
network.
|
||||
|
||||
Live at **[docs.rethinkstudios.io](https://docs.rethinkstudios.io)**.
|
||||
|
||||
## What this is
|
||||
|
||||
A static documentation site built with [MkDocs](https://www.mkdocs.org/) +
|
||||
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). Markdown lives
|
||||
under `docs/`, builds to a static `site/`, and is served by the reverse proxy at
|
||||
the subdomain — no app process, just static HTML/CSS/JS.
|
||||
|
||||
The **Libraries** page is the one dynamic part: it fetches the `rethink-public`
|
||||
repo list from the Gitea API client-side at view time, so new libraries appear on
|
||||
the next page load with no rebuild. Everything else is static markdown.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
mkdocs build # -> static site/
|
||||
mkdocs serve # local preview at http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
- One topic per page, grouped under `docs/`, wired into the nav in `mkdocs.yml`.
|
||||
- **Public — sanitize:** no real hostnames, internal IPs, secrets, or exact
|
||||
topology. Use placeholders (`<dev>`, `<project>`, `/srv/...`).
|
||||
- Markdown: trailing newline, no trailing whitespace, LF line endings.
|
||||
- Commits signed (`git commit -s`).
|
||||
10
docs/assets/logo.svg
Normal file
10
docs/assets/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.0 KiB |
143
docs/deploy.md
143
docs/deploy.md
@ -4,76 +4,120 @@
|
||||
> **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever
|
||||
> you've built.
|
||||
|
||||
**Eligible:** APIs, websites, applets, bots, monitors.
|
||||
|
||||
The whole network runs on a few simple, consistent rules. Get your container to
|
||||
follow them and deploying is mostly handing us a `compose.yaml`.
|
||||
!!! tip "Eligible"
|
||||
APIs, websites, applets, bots, monitors. The whole network runs on a few
|
||||
simple, consistent rules — get your container to follow them and deploying is
|
||||
mostly handing us a `compose.yaml`.
|
||||
|
||||
## Docker — the services account
|
||||
|
||||
Every service runs containerized as the shared **`services`** account:
|
||||
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
|
||||
runs cleanly as that account:
|
||||
|
||||
- `user: "1337:1337"` in compose.
|
||||
- `chmod -R a+rwX /app` in the Dockerfile (covers non-mounted dirs — see the
|
||||
bind-mount note below).
|
||||
- `HOME=/tmp`.
|
||||
- **No** in-container `user`/`useradd` — don't bake a user into the image.
|
||||
runs cleanly as that account.
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
ENV HOME=/tmp
|
||||
ENV HOME=/tmp # (1)!
|
||||
|
||||
WORKDIR /app
|
||||
# git in the build if you pip-install from git
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git \
|
||||
&& rm -rf /var/lib/apt/lists/* # (2)!
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt # (3)!
|
||||
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir . \
|
||||
&& chmod -R a+rwX /app
|
||||
RUN chmod -R a+rwX /app # (4)!
|
||||
CMD ["python", "-m", "yourapp"]
|
||||
```
|
||||
|
||||
1. `HOME=/tmp` — the `services` account has no home dir; anything writing to
|
||||
`$HOME` (caches, configs) needs a writable target.
|
||||
2. Include `git` **only if the container itself needs it** — e.g. you
|
||||
`pip install` from git, or the app shells out to git at runtime. The build and
|
||||
host always have git; this line is about what's *inside* the image.
|
||||
3. Install deps **before** copying the code (see [layer
|
||||
caching](#layer-caching)).
|
||||
4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what
|
||||
"uid-agnostic" means.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
yourapp:
|
||||
build: .
|
||||
user: "1337:1337"
|
||||
user: "1337:1337" # (1)!
|
||||
environment:
|
||||
HOME: /tmp
|
||||
volumes:
|
||||
- /srv/configs/<project>:/app/config:ro # host-managed, read-only
|
||||
- /srv/<dev>/<project>:/app/logs # live + rolled logs
|
||||
- yourapp-data:/app/data # named volume — the rest
|
||||
- /srv/configs/<project>:/app/config:ro # (2)!
|
||||
- /srv/logs/<dev>/<project>:/app/logs # (3)!
|
||||
- yourapp-data:/app/data # (4)!
|
||||
|
||||
volumes:
|
||||
yourapp-data:
|
||||
```
|
||||
|
||||
1. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a
|
||||
user into the image; set it here.
|
||||
2. Configs: host-managed bind mount, mounted **read-only**.
|
||||
3. Logs: bind mount — live and rolled, scraped for monitoring.
|
||||
4. Everything else: a **named volume**. Docker owns it, so there are no host
|
||||
permissions to fiddle with.
|
||||
|
||||
## Paths and mounts
|
||||
|
||||
- **Configs** → `/srv/configs/<project>/` — bind mount, host-managed.
|
||||
- **Logs** → `/srv/<dev>/<project>/` — bind mount; live and rolled, scraped for
|
||||
monitoring.
|
||||
- **Everything else** (caches, browser profiles, scratch) → **named volumes**.
|
||||
Docker manages ownership, so there are no host permissions to fiddle with.
|
||||
| What | Where | How |
|
||||
| --- | --- | --- |
|
||||
| Configs | `/srv/configs/<project>/` | bind mount, host-managed, read-only |
|
||||
| Logs | `/srv/logs/<dev>/<project>/` | bind mount; live + rolled, scraped |
|
||||
| Caches, profiles, scratch | named volume | Docker manages ownership |
|
||||
|
||||
## Permissions — the bind-mount footgun
|
||||
!!! note "If your service won't start or its logs aren't persisting"
|
||||
That's usually a host-side bind-mount **ownership** thing — the kind of detail
|
||||
**we sort out at deploy time**, not something you need to chown or provision.
|
||||
If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix
|
||||
the mount perms. Stick to a clean `compose.yaml` and let us handle the host.
|
||||
|
||||
`docker compose up` does **not** create bind-mount directories as you. If a
|
||||
bind-mount source is missing, the Docker daemon (**root**) creates it **as
|
||||
root** — and your container (**1337**) then can't write it. Logs fall back to
|
||||
console-only, caches re-download every run.
|
||||
## Layer caching
|
||||
|
||||
So:
|
||||
Copy `requirements.txt` and `pip install` **before** `COPY . .`. Docker caches
|
||||
layers in order, so deps only reinstall when `requirements.txt` changes — not on
|
||||
every code edit. Get this backwards and every one-line change triggers a full
|
||||
dependency reinstall.
|
||||
|
||||
- **Bind-mount sources (configs, logs) must EXIST and be 1337-owned _before_
|
||||
`up`.** This is handled at provisioning, not a per-deploy chown hook.
|
||||
- **Named volumes avoid this entirely** — use them for anything that doesn't need
|
||||
host visibility.
|
||||
- The Dockerfile `chmod -R a+rwX /app` only covers **non-mounted** dirs. A bind
|
||||
mount overrides the image directory with the host directory, so for mounted
|
||||
paths the **host-side ownership wins**.
|
||||
```dockerfile
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt # cached until deps change
|
||||
COPY . . # changes every build
|
||||
```
|
||||
|
||||
## Subprocess and browser workloads
|
||||
|
||||
Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra
|
||||
knobs in compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
yourbot:
|
||||
build: .
|
||||
user: "1337:1337"
|
||||
init: true # (1)!
|
||||
shm_size: "2gb" # (2)!
|
||||
mem_limit: "4g" # (3)!
|
||||
```
|
||||
|
||||
1. Runs **tini** as PID 1 to reap zombie subprocesses and forward signals.
|
||||
Without it, spawned Chrome/Xvfb processes leak as zombies.
|
||||
2. Chrome and most headless browsers crash on Docker's default **64 MB**
|
||||
`/dev/shm`. Bump it for any browser workload.
|
||||
3. Bound memory — especially when each worker spawns a browser. Raise it as
|
||||
worker count grows.
|
||||
|
||||
!!! warning "The PID-1 gotcha with shell-wrapper CMDs"
|
||||
If your `CMD` is a shell-script wrapper (e.g. `xvfb-run ...`), it must **not**
|
||||
be PID 1, or the real process dies on startup. `init: true` is exactly what
|
||||
fixes this — tini takes PID 1, and your wrapper runs as a normal child.
|
||||
|
||||
## What your compose / Dockerfile needs
|
||||
|
||||
@ -83,16 +127,21 @@ So:
|
||||
- secrets bind-mounted **`:ro`**
|
||||
- `HOME=/tmp`
|
||||
- `chmod -R a+rwX /app`
|
||||
- `git` in the build if you `pip install` from git
|
||||
- deps installed **before** the code copy (layer caching)
|
||||
- `git` in the image **if the container needs it**
|
||||
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
|
||||
|
||||
## Secrets
|
||||
|
||||
We do **not** commit secrets (usually, lol). The rule:
|
||||
|
||||
- Secrets stay **gitignored**.
|
||||
- They're placed on the host at `/srv/configs/<project>/`.
|
||||
- They're bind-mounted **read-only** at runtime.
|
||||
- **Never** baked into the image — add them to `.dockerignore` so `COPY . .`
|
||||
can't grab them.
|
||||
!!! warning "Secrets never go in the image"
|
||||
We do **not** commit secrets (usually, lol). They stay **gitignored**, live on
|
||||
the host at `/srv/configs/<project>/`, and are bind-mounted **read-only** at
|
||||
runtime. Add them to `.dockerignore` so a `COPY . .` can't sweep them into a
|
||||
layer.
|
||||
|
||||
Rotating a secret = edit the host file and restart. No rebuild.
|
||||
|
||||
```bash
|
||||
vim /srv/configs/<project>/secrets.env # edit on the host
|
||||
docker compose restart yourapp # pick up the change — no rebuild
|
||||
```
|
||||
|
||||
145
docs/environments.md
Normal file
145
docs/environments.md
Normal file
@ -0,0 +1,145 @@
|
||||
# Virtual environments
|
||||
|
||||
How we keep a project's Python isolated — from the system Python and from every
|
||||
other project. The rule underneath all of it: **never install into, or upgrade,
|
||||
the system Python.** The OS depends on it; a project must never touch it.
|
||||
|
||||
!!! danger "Leave the system Python alone"
|
||||
No `sudo pip install`, no upgrading the system interpreter for a project. If a
|
||||
project needs a different version or a package, that goes in a **virtual
|
||||
environment** — never the system one. Breaking system Python can break the OS.
|
||||
|
||||
## Project-based isolation
|
||||
|
||||
Every project runs against its own isolated environment. There are three ways that
|
||||
happens, depending on where the project runs:
|
||||
|
||||
=== "Local `.venv`"
|
||||
|
||||
A virtualenv living in the repo. Simplest for day-to-day local work.
|
||||
|
||||
```bash
|
||||
python -m venv .venv # create it (once)
|
||||
source .venv/bin/activate # activate for this shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Keep `.venv/` **gitignored** — it's per-machine, never committed.
|
||||
|
||||
=== "Makefile-driven"
|
||||
|
||||
Wrap the venv in a `make` target so every dev (and CI) sets up identically —
|
||||
no "did you activate it?" drift.
|
||||
|
||||
```makefile
|
||||
VENV := .venv
|
||||
PY := $(VENV)/bin/python
|
||||
|
||||
$(VENV): requirements.txt
|
||||
python -m venv $(VENV)
|
||||
$(PY) -m pip install -r requirements.txt
|
||||
|
||||
.PHONY: run
|
||||
run: $(VENV)
|
||||
$(PY) -m yourapp
|
||||
```
|
||||
|
||||
`make run` creates the venv if missing, installs deps, and runs — all against
|
||||
the isolated interpreter, no manual activation.
|
||||
|
||||
=== "Docker"
|
||||
|
||||
The container **is** the isolation — its own filesystem, its own interpreter,
|
||||
nothing shared with the host. You don't need a `.venv` inside an image; install
|
||||
straight into the container's Python.
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
```
|
||||
|
||||
This is how things run in production — see the [Deploy guide](deploy.md) for the
|
||||
full container standard (uid 1337, mounts, layer caching).
|
||||
|
||||
!!! tip "Which one?"
|
||||
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable
|
||||
setup across the team, **Docker** for anything that ships. They're not
|
||||
exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for
|
||||
deploy.
|
||||
|
||||
## Local dev with pyenv
|
||||
|
||||
For local work you also need the right **Python version**, not just isolated deps.
|
||||
We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and
|
||||
switches versions per-project without touching the system Python.
|
||||
|
||||
- **Per-project pinning:** a `.python-version` file in a repo makes pyenv
|
||||
auto-select that interpreter when you `cd` in — everyone on the project runs the
|
||||
same one.
|
||||
- **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an
|
||||
isolated version *and* isolated deps.
|
||||
|
||||
### Install
|
||||
|
||||
Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation)
|
||||
for the installer and build dependencies — no point reproducing it here. Then add
|
||||
the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell.
|
||||
|
||||
### Shell init
|
||||
|
||||
Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and
|
||||
auto-version-switching won't work:
|
||||
|
||||
```bash
|
||||
# pyenv — Python version management
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
eval "$(pyenv init --path)" # (1)!
|
||||
eval "$(pyenv init -)" # (2)!
|
||||
eval "$(pyenv virtualenv-init -)" # (3)!
|
||||
```
|
||||
|
||||
1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected
|
||||
version instead of the system one.
|
||||
2. Shell integration — command rehashing and completion.
|
||||
3. Auto-activates a project's virtualenv on `cd` — **only** if you use
|
||||
`pyenv-virtualenv`. Drop this line if you don't.
|
||||
|
||||
### Everyday use
|
||||
|
||||
```bash
|
||||
pyenv install 3.10.14 # install a version (one-time)
|
||||
pyenv install --list # see available versions
|
||||
cd <project>
|
||||
pyenv local 3.10.14 # writes .python-version -> auto-selects here
|
||||
python --version # confirms the pinned version
|
||||
```
|
||||
|
||||
- **`pyenv local <ver>`** per project — commit the `.python-version` so the team
|
||||
matches.
|
||||
- **`pyenv global <ver>`** for your default outside any project.
|
||||
|
||||
## Shell quality-of-life extras
|
||||
|
||||
A couple of optional lines worth having alongside pyenv:
|
||||
|
||||
```bash
|
||||
# flake8 with our shared config (max line 120)
|
||||
alias flake8='flake8 --config ~/.config/flake8'
|
||||
|
||||
# local bins on PATH (pip --user installs, npm globals)
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
```
|
||||
|
||||
- The **flake8 alias** keeps everyone linting with the same config (our 120
|
||||
max-line, etc. — see [Standards](standards.md#files-and-style)).
|
||||
- The **`.local/bin` / npm-global** PATH lines stop "command not found" after a
|
||||
`pip install --user` or a global npm install.
|
||||
|
||||
!!! note "More shell setup"
|
||||
This is the Python-env slice. The fuller dev shell setup — the WSL/Windows
|
||||
clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on
|
||||
the [Workflow](workflow.md#handy-shell-setup) page.
|
||||
@ -1,4 +1,4 @@
|
||||
# Rethink Studios Handbook
|
||||
# rethink development
|
||||
|
||||
The public reference for building and shipping with Rethink Studios: our shared
|
||||
libraries, our coding standards, and how to deploy a project on our network.
|
||||
@ -25,6 +25,20 @@ out of it; examples use placeholders like `<dev>`, `<project>`, and `/srv/...`.
|
||||
House coding standards — file hygiene, docstrings, type hints, linting, and
|
||||
how we handle errors.
|
||||
|
||||
- :material-sitemap: __[Workflow](workflow.md)__
|
||||
|
||||
---
|
||||
|
||||
How we actually work day to day — our Gitea, git habits, and the
|
||||
plan-in-chat / build-in-Claude-Code flow, plus shell setup.
|
||||
|
||||
- :material-language-python: __[Virtual environments](environments.md)__
|
||||
|
||||
---
|
||||
|
||||
Project-based Python isolation — local `.venv`, Makefile, or Docker — and
|
||||
local version management with pyenv.
|
||||
|
||||
- :material-rocket-launch: __[Deploy](deploy.md)__
|
||||
|
||||
---
|
||||
|
||||
@ -11,10 +11,44 @@ the repo, where the README and tags live.
|
||||
|
||||
## Install
|
||||
|
||||
Pin a tag in your dependencies — never an unpinned branch:
|
||||
|
||||
```
|
||||
<lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag>
|
||||
```
|
||||
|
||||
??? example "Using a library — install, import, go"
|
||||
|
||||
Add it to your project's deps (e.g. `pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"aioweb @ git+https://git.rethinkstudios.io/rethink-public/aioweb.git@v0.3.1",
|
||||
]
|
||||
```
|
||||
|
||||
Then use it — the README in each repo has the real surface; this is the shape:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from aioweb import Session
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""fetch a page through the shared async http session"""
|
||||
async with Session() as web:
|
||||
resp = await web.get("https://example.test")
|
||||
print(resp.status, len(resp.content))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Need to bump a lib? Change the `@<tag>` and reinstall — versions live with the
|
||||
lib, not in these docs.
|
||||
|
||||
<div id="lib-list" markdown="0">
|
||||
<p class="lib-status">Loading libraries from Gitea…</p>
|
||||
</div>
|
||||
@ -55,12 +89,17 @@ the repo, where the README and tags live.
|
||||
var rows = libs.map(function (r) {
|
||||
var url = r.html_url || (REPO_BASE + "/" + r.name);
|
||||
var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>";
|
||||
var lang = r.language
|
||||
? '<span class="rt-lang">' + escapeHtml(r.language) + "</span>"
|
||||
: '<span class="rt-lang rt-lang--none">—</span>';
|
||||
return '<tr><td><a href="' + escapeHtml(url) + '"><code>' +
|
||||
escapeHtml(r.name) + '</code></a></td><td>' + desc + '</td></tr>';
|
||||
escapeHtml(r.name) + "</code></a></td><td>" + desc +
|
||||
"</td><td>" + lang + "</td></tr>";
|
||||
}).join("");
|
||||
el.innerHTML =
|
||||
'<table><thead><tr><th>Library</th><th>What it does</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody></table>';
|
||||
'<div class="rt-lib-table"><table>' +
|
||||
"<thead><tr><th>Library</th><th>What it does</th><th>Language</th></tr></thead>" +
|
||||
"<tbody>" + rows + "</tbody></table></div>";
|
||||
}
|
||||
|
||||
function fail() {
|
||||
@ -84,11 +123,3 @@ the repo, where the README and tags live.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
!!! info "If the live list is empty or stale"
|
||||
The fetch runs in your browser against the Gitea API and needs the org repos
|
||||
to be readable unauthenticated (they are — it's a public org) and CORS to
|
||||
allow the docs domain. If the list won't load, browse the org directly at
|
||||
[git.rethinkstudios.io/rethink-public](https://git.rethinkstudios.io/rethink-public).
|
||||
The fallback for a disabled live fetch is a webhook rebuild — a push to any
|
||||
`rethink-public` repo triggers a site rebuild.
|
||||
|
||||
@ -3,40 +3,129 @@
|
||||
The house standards every Rethink Studios project follows. They keep our code
|
||||
consistent, readable, and predictable across the whole suite.
|
||||
|
||||
## File hygiene
|
||||
## Files and style
|
||||
|
||||
- Files end with a **single trailing newline** (LF / Unix line endings).
|
||||
- **No trailing whitespace** on any line.
|
||||
- Files end with a **single trailing newline** (LF / Unix line endings), and
|
||||
carry **no trailing whitespace** on any line.
|
||||
- **4 spaces** for indentation by default. Respect language norms — TS/JS use
|
||||
**2 spaces**, Go uses **tabs** — and when editing an existing file, follow that
|
||||
file's existing indentation.
|
||||
- **flake8 clean**, max line length **120**. **Type hints** on public functions.
|
||||
|
||||
## Indentation
|
||||
A well-formed module — module docstring, type hints, lowercase-start docstring:
|
||||
|
||||
- **4 spaces** by default.
|
||||
- Respect language norms: TS/JS use **2 spaces**, Go uses **tabs**.
|
||||
- When editing an existing file, follow that file's existing indentation.
|
||||
```python
|
||||
"""async key/value store backed by a single json file"""
|
||||
|
||||
## Docstrings and comments
|
||||
from pathlib import Path
|
||||
|
||||
- Public functions get a **docstring**. Style: **lowercase-start, no trailing
|
||||
period**.
|
||||
- Keep inline comments minimal — prefer docstrings. Use an inline comment only
|
||||
|
||||
def load_state(path: Path) -> dict[str, str]:
|
||||
"""read the json state file, returning an empty dict if it's missing"""
|
||||
if not path.exists():
|
||||
return {}
|
||||
return _read_json(path)
|
||||
```
|
||||
|
||||
flake8 tells you the moment you drift — keep the tree clean:
|
||||
|
||||
```text
|
||||
$ flake8
|
||||
./aiokv/store.py:14:80: E501 line too long (96 > 79 characters)
|
||||
./aiokv/store.py:22:1: F401 'json' imported but unused
|
||||
./aiokv/store.py:31:5: E303 too many blank lines (3)
|
||||
```
|
||||
|
||||
!!! tip "Run it locally"
|
||||
Wire the shared config into an alias so every project lints the same way:
|
||||
`alias flake8='flake8 --config ~/.config/flake8'` (max line 120). See the
|
||||
[Workflow](workflow.md#handy-shell-setup) page.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Public functions get a **docstring** — **lowercase-start, no trailing period**.
|
||||
- Keep inline comments minimal; prefer docstrings. Use an inline comment only
|
||||
where the code is genuinely complex.
|
||||
- Each module/file states its scope and purpose via a **module docstring**
|
||||
(header string) — **not** a license or copyright header.
|
||||
- Every library and project has a **README** with the install line, what it does,
|
||||
and a usage example.
|
||||
|
||||
## READMEs
|
||||
!!! note "Licensing — no per-file headers"
|
||||
Don't prepend license/copyright boilerplate to source files. If a repo needs a
|
||||
license, it's a single top-level **`LICENSE`** file — never repeated per file.
|
||||
Most internal repos don't carry one; add it only when a repo is meant for
|
||||
outside use and the terms are decided.
|
||||
|
||||
Every library and project has a defined **README** covering:
|
||||
=== "Do"
|
||||
|
||||
- the install line,
|
||||
- what it does,
|
||||
- a usage example.
|
||||
```python
|
||||
def mask_secret(value: str, keep: int = 4) -> str:
|
||||
"""mask all but the last ``keep`` characters of a secret"""
|
||||
if len(value) <= keep:
|
||||
return "*" * len(value)
|
||||
return "*" * (len(value) - keep) + value[-keep:]
|
||||
```
|
||||
|
||||
## Linting and types
|
||||
=== "Don't"
|
||||
|
||||
- **flake8 clean**, max line length **120**.
|
||||
- **Type hints** on public functions.
|
||||
```python
|
||||
# Masks a secret. <- license-header-style noise, capitalized, trailing period
|
||||
def mask_secret(value, keep=4): # no type hints
|
||||
# loop over the chars and hide them
|
||||
return "*" * (len(value) - keep) + value[-keep:] # crashes if short
|
||||
```
|
||||
|
||||
## Error handling
|
||||
## Quality and error handling
|
||||
|
||||
- **Fail loud** — never swallow exceptions.
|
||||
- Catch the **specific** exception, and **log** it.
|
||||
**Fail loud** — never swallow exceptions. Catch the **specific** exception and
|
||||
**log** it; don't paper over failures with a bare `except`.
|
||||
|
||||
=== "Do"
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fetch(url: str) -> bytes:
|
||||
"""fetch ``url``, logging and re-raising on failure"""
|
||||
try:
|
||||
return _client.get(url).content
|
||||
except TimeoutError:
|
||||
log.warning("fetch timed out: %s", url)
|
||||
raise
|
||||
```
|
||||
|
||||
=== "Don't"
|
||||
|
||||
```python
|
||||
def fetch(url):
|
||||
try:
|
||||
return _client.get(url).content
|
||||
except Exception:
|
||||
pass # swallowed — the caller has no idea anything broke
|
||||
```
|
||||
|
||||
When something does break, a loud failure gives you a real traceback to act on:
|
||||
|
||||
```python-traceback
|
||||
Traceback (most recent call last):
|
||||
File "run.py", line 42, in <module>
|
||||
data = fetch("https://example.test/feed")
|
||||
File "aioweb/session.py", line 88, in fetch
|
||||
return self._client.get(url).content
|
||||
TimeoutError: request timed out after 30s
|
||||
```
|
||||
|
||||
…and the log line that precedes it tells you where to look:
|
||||
|
||||
```text
|
||||
2026-06-29 14:03:11,204 WARNING aioweb.session fetch timed out: https://example.test/feed
|
||||
```
|
||||
|
||||
!!! note "Logging belongs to the app, not the library"
|
||||
Libraries **emit only** — `log = logging.getLogger(__name__)` and nothing
|
||||
else. Handlers, levels, and formatting are configured once at the
|
||||
application entry point, so a lib never dictates how its host logs.
|
||||
|
||||
236
docs/stylesheets/extra.css
Normal file
236
docs/stylesheets/extra.css
Normal file
@ -0,0 +1,236 @@
|
||||
/* a rethink development — mirror of the rethink Gitea theme
|
||||
palette pulled from git.rethinkstudios.io (data-theme="rethink") */
|
||||
|
||||
:root {
|
||||
--rt-body: #0a0f1f; /* deep navy page background */
|
||||
--rt-nav: #061541; /* darker header/nav background */
|
||||
--rt-surface: #0e1530; /* cards / code blocks, a touch above body */
|
||||
--rt-text: #eef1f6; /* near-white body text */
|
||||
--rt-muted: #aab4c5; /* secondary text */
|
||||
--rt-primary: #569bcc; /* links / primary blue */
|
||||
--rt-primary-dark: #4a8cbb;
|
||||
--rt-primary-light: #6ba9d5;
|
||||
--rt-accent: #55bbff; /* bright cyan-blue accent */
|
||||
--rt-border: #294274; /* row dividers / borders */
|
||||
}
|
||||
|
||||
/* Map the rethink palette onto Material's slate scheme. */
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-hue: 222;
|
||||
|
||||
--md-default-bg-color: var(--rt-body);
|
||||
--md-default-fg-color: var(--rt-text);
|
||||
--md-default-fg-color--light: var(--rt-muted);
|
||||
--md-default-fg-color--lighter: rgba(238, 241, 246, 0.32);
|
||||
--md-default-fg-color--lightest: rgba(238, 241, 246, 0.12);
|
||||
|
||||
--md-primary-fg-color: var(--rt-nav);
|
||||
--md-primary-fg-color--light: var(--rt-primary-light);
|
||||
--md-primary-fg-color--dark: var(--rt-nav);
|
||||
--md-primary-bg-color: var(--rt-text);
|
||||
--md-primary-bg-color--light: var(--rt-muted);
|
||||
|
||||
--md-accent-fg-color: var(--rt-accent);
|
||||
--md-accent-fg-color--transparent: rgba(85, 187, 255, 0.1);
|
||||
|
||||
--md-typeset-color: var(--rt-text);
|
||||
--md-typeset-a-color: var(--rt-primary);
|
||||
|
||||
--md-code-bg-color: var(--rt-surface);
|
||||
--md-code-fg-color: #d6deeb;
|
||||
|
||||
--md-footer-bg-color: var(--rt-nav);
|
||||
--md-footer-bg-color--dark: var(--rt-body);
|
||||
}
|
||||
|
||||
/* Header / top nav: match Gitea's dark-blue bar. */
|
||||
.md-header,
|
||||
.md-tabs {
|
||||
background-color: var(--rt-nav);
|
||||
color: var(--rt-text);
|
||||
}
|
||||
|
||||
/* Links hover to the bright accent, like Gitea. */
|
||||
.md-typeset a:hover {
|
||||
color: var(--rt-accent);
|
||||
}
|
||||
|
||||
/* Sidebar nav active item picks up the blue. */
|
||||
.md-nav__link--active,
|
||||
.md-nav__item .md-nav__link--active {
|
||||
color: var(--rt-primary);
|
||||
}
|
||||
|
||||
/* Square the corners — Gitea uses radius 0 throughout. */
|
||||
.md-typeset .admonition,
|
||||
.md-typeset details,
|
||||
.md-typeset pre > code,
|
||||
.md-typeset .highlight,
|
||||
.md-typeset table:not([class]),
|
||||
.md-search__form,
|
||||
.md-typeset .tabbed-set > input:checked + label,
|
||||
.md-typeset code {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Subtle blue row dividers in tables, echoing the repo list. */
|
||||
.md-typeset table:not([class]) {
|
||||
border: 1px solid var(--rt-border);
|
||||
}
|
||||
.md-typeset table:not([class]) th {
|
||||
background-color: var(--rt-nav);
|
||||
color: var(--rt-text);
|
||||
}
|
||||
.md-typeset table:not([class]) td {
|
||||
border-top: 1px solid rgba(41, 66, 116, 0.5);
|
||||
}
|
||||
|
||||
/* Search field on the dark bar. */
|
||||
.md-search__input {
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.md-search__input::placeholder {
|
||||
color: rgba(238, 241, 246, 0.6);
|
||||
}
|
||||
|
||||
/* Admonitions — Gitea-style: brand-orange warning, blue note, etc.
|
||||
The lambda logo's orange (#ffae42 -> #f57c00) drives the warning/danger look. */
|
||||
.md-typeset .admonition,
|
||||
.md-typeset details {
|
||||
border-width: 0 0 0 .2rem;
|
||||
background-color: var(--rt-surface);
|
||||
}
|
||||
|
||||
/* warning / caution: the orange Gitea callout */
|
||||
.md-typeset .admonition.warning,
|
||||
.md-typeset details.warning,
|
||||
.md-typeset .admonition.caution,
|
||||
.md-typeset details.caution {
|
||||
border-color: #f57c00;
|
||||
}
|
||||
.md-typeset .warning > .admonition-title,
|
||||
.md-typeset .warning > summary,
|
||||
.md-typeset .caution > .admonition-title,
|
||||
.md-typeset .caution > summary {
|
||||
background-color: rgba(245, 124, 0, 0.12);
|
||||
}
|
||||
.md-typeset .warning > .admonition-title::before,
|
||||
.md-typeset .caution > .admonition-title::before {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
|
||||
/* danger: a hotter orange-red for true footguns */
|
||||
.md-typeset .admonition.danger,
|
||||
.md-typeset details.danger {
|
||||
border-color: #e8590c;
|
||||
}
|
||||
.md-typeset .danger > .admonition-title,
|
||||
.md-typeset .danger > summary {
|
||||
background-color: rgba(232, 89, 12, 0.14);
|
||||
}
|
||||
|
||||
/* note / info / tip: blue + cyan from the rethink palette */
|
||||
.md-typeset .admonition.note,
|
||||
.md-typeset details.note,
|
||||
.md-typeset .admonition.info,
|
||||
.md-typeset details.info {
|
||||
border-color: var(--rt-primary);
|
||||
}
|
||||
.md-typeset .note > .admonition-title,
|
||||
.md-typeset .info > .admonition-title {
|
||||
background-color: rgba(86, 155, 204, 0.12);
|
||||
}
|
||||
.md-typeset .admonition.tip,
|
||||
.md-typeset details.tip,
|
||||
.md-typeset .admonition.example,
|
||||
.md-typeset details.example {
|
||||
border-color: var(--rt-accent);
|
||||
}
|
||||
.md-typeset .tip > .admonition-title,
|
||||
.md-typeset .example > summary {
|
||||
background-color: rgba(85, 187, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Live libraries table — JS-injected, so it needs explicit Material-style rules
|
||||
(Material only auto-styles markdown tables, not innerHTML ones). */
|
||||
.rt-lib-table {
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.rt-lib-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--rt-border);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.rt-lib-table th,
|
||||
.rt-lib-table td {
|
||||
text-align: left;
|
||||
padding: 0.7em 1em;
|
||||
vertical-align: top;
|
||||
}
|
||||
.rt-lib-table td:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rt-lib-table thead th {
|
||||
background-color: var(--rt-nav);
|
||||
color: var(--rt-text);
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid var(--rt-border);
|
||||
}
|
||||
.rt-lib-table tbody tr {
|
||||
border-top: 1px solid rgba(41, 66, 116, 0.5);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.rt-lib-table tbody tr:hover {
|
||||
background-color: rgba(86, 155, 204, 0.08);
|
||||
}
|
||||
.rt-lib-table td code {
|
||||
background-color: rgba(85, 187, 255, 0.12);
|
||||
color: #cfe6ff;
|
||||
padding: 0.2em 0.55em;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.rt-lib-table a {
|
||||
color: var(--rt-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rt-lib-table a:hover code {
|
||||
background-color: rgba(85, 187, 255, 0.22);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Language badge in the libs table. */
|
||||
.rt-lang {
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.5em;
|
||||
border: 1px solid var(--rt-border);
|
||||
border-radius: 0;
|
||||
background-color: rgba(86, 155, 204, 0.1);
|
||||
color: var(--rt-primary-light);
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rt-lang--none {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: var(--rt-muted);
|
||||
}
|
||||
|
||||
/* Code annotation markers (the (1) callouts) in brand blue. */
|
||||
.md-typeset .md-annotation__index {
|
||||
background-color: var(--rt-primary);
|
||||
color: var(--rt-body);
|
||||
}
|
||||
|
||||
/* Landing-page cards: surface tint + blue border like Gitea panels. */
|
||||
.md-typeset .grid.cards > :is(ul, ol) > li,
|
||||
.md-typeset .grid > .card {
|
||||
background-color: var(--rt-surface);
|
||||
border: 1px solid var(--rt-border);
|
||||
border-radius: 0;
|
||||
}
|
||||
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
|
||||
border-color: var(--rt-primary);
|
||||
}
|
||||
243
docs/workflow.md
Normal file
243
docs/workflow.md
Normal file
@ -0,0 +1,243 @@
|
||||
# Workflow
|
||||
|
||||
How we actually work day to day at Rethink Studios — where code lives, how we use
|
||||
git, the AI-assisted dev flow we recommend, and the shell setup that ties it
|
||||
together. This is the **our-flavored** version: why *we* do it this way and how
|
||||
*our* setup is wired. For the truly generic parts (installing WSL, learning git),
|
||||
we link the official docs rather than reteach them.
|
||||
|
||||
!!! info "Public, sanitized"
|
||||
Examples use placeholders — `<you>`, `<key>`, `dev@<you>`, `/mnt/c/<your>/...`.
|
||||
Swap in your own real values locally; never commit personal paths, key names,
|
||||
or emails.
|
||||
|
||||
## Why git, why WSL
|
||||
|
||||
**Git** is the backbone of everything here. Version history and branching are the
|
||||
obvious part, but our whole deploy model is built on git too: we `pip install`
|
||||
libraries straight from git by tag, servers pull via per-repo deploy keys, and
|
||||
changes flow `develop → main` before they ship. If you know git, you already
|
||||
understand how our code moves from your machine to production.
|
||||
|
||||
**WSL2 (Ubuntu)** is where we develop — a real Linux toolchain on a Windows
|
||||
desktop. You get native Linux tooling (the same environment our servers run) plus
|
||||
the Windows apps you actually use day to day. Install WSL2 from Microsoft's docs;
|
||||
this page documents the *our-setup* layer that goes on top.
|
||||
|
||||
- [Install WSL (Microsoft)](https://learn.microsoft.com/windows/wsl/install)
|
||||
- [VS Code Remote — WSL](https://code.visualstudio.com/docs/remote/wsl)
|
||||
|
||||
## Signing up on our Gitea
|
||||
|
||||
Our code lives on **Gitea** at
|
||||
[git.rethinkstudios.io](https://git.rethinkstudios.io). Two orgs you'll use:
|
||||
|
||||
- **[rethink-public](https://git.rethinkstudios.io/rethink-public)** — public
|
||||
libraries, resources, and assets.
|
||||
- **[rethink-software](https://git.rethinkstudios.io/rethink-software)** — our
|
||||
applications.
|
||||
|
||||
**Get an account:**
|
||||
|
||||
1. Register at [git.rethinkstudios.io](https://git.rethinkstudios.io) and verify
|
||||
your email.
|
||||
2. Add your **SSH public key** under *Settings → SSH / GPG Keys* so you can clone
|
||||
and push over SSH.
|
||||
3. For servers, we use a **per-repo deploy-key** model rather than your personal
|
||||
key — see the [Deploy guide](deploy.md) for how a box gets read access to just
|
||||
the repos it needs.
|
||||
|
||||
## Our git vs. public git (GitHub / GitLab)
|
||||
|
||||
It's the same git — just a different host. The thing most devs trip on is
|
||||
**identity and keys per host**: you may have a GitHub identity *and* a Gitea
|
||||
identity on one machine, and commits need to be attributed (and signed/pushed)
|
||||
with the right one per project.
|
||||
|
||||
We solve that with **per-repo local git config** — run a small alias inside a repo
|
||||
to set its local user and the SSH key it pushes with (see
|
||||
[per-project git identity](#per-project-git-identity) below). No global identity
|
||||
juggling.
|
||||
|
||||
Our conventions, in short:
|
||||
|
||||
- **Signed commits** — `git commit -s`.
|
||||
- **No AI co-author trailer** on your own work. (Intern/dev work you're crediting
|
||||
gets that dev's `Co-Authored-By` — nothing else.)
|
||||
- **`develop` is staging**, merge to **`main`** via MR when it's ready to ship.
|
||||
- **Libraries install from git by tag** — pin a version in your deps, bump the
|
||||
tag when the lib releases.
|
||||
|
||||
!!! warning "Set your per-repo identity *before* the first commit"
|
||||
One machine often carries more than one Gitea identity/key. If you forget to
|
||||
run `gitsetup` in a fresh clone, your commits attribute to the wrong
|
||||
user — or push with the wrong key and bounce. Run it right after cloning;
|
||||
see [per-project git identity](#handy-shell-setup).
|
||||
|
||||
## Git basics (our-flavored)
|
||||
|
||||
Not a git tutorial — just how the everyday loop looks against our Gitea. For the
|
||||
generic command reference, keep the
|
||||
[Git cheat sheet](https://training.github.com/downloads/github-git-cheat-sheet/)
|
||||
or the [Pro Git book](https://git-scm.com/book) handy.
|
||||
|
||||
**Clone** over SSH (the alias maps to a key — see the shell setup):
|
||||
|
||||
```bash
|
||||
git clone git@<alias>:rethink-public/<repo>.git
|
||||
```
|
||||
|
||||
**The everyday loop:**
|
||||
|
||||
```bash
|
||||
git switch -c <feature> # branch off
|
||||
# ...make a logical change...
|
||||
git commit -s -m "..." # commit, signed
|
||||
git push -u origin <feature> # push
|
||||
# open an MR: develop -> main
|
||||
```
|
||||
|
||||
**Commit small and often** — one commit per logical change, not a giant
|
||||
end-of-day dump. Small commits are easier to review, revert, and `git bisect`
|
||||
when something breaks.
|
||||
|
||||
**Read history** as a graph with the `gl` alias below:
|
||||
|
||||
```bash
|
||||
gl # git log --graph, oneline, decorated
|
||||
```
|
||||
|
||||
## The dev workflow
|
||||
|
||||
This is the part that's distinctly *ours*. AI is a force multiplier, but only with
|
||||
discipline around it — the two habits that matter most are **plan before you
|
||||
build** and **verify by executing, not asserting**.
|
||||
|
||||
You can use whatever AI you like — but we **recommend Claude** (via
|
||||
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview), in the
|
||||
terminal or VS Code). The whole project structure we recommend below — the
|
||||
`.claude/` folder, `CLAUDE.md` instructions, numbered specs — is built on that
|
||||
preference: it's designed around how Claude Code reads project context and takes
|
||||
handoffs. Other tools can read these files too, but the convention assumes
|
||||
Claude-first.
|
||||
|
||||
**Plan here, build in Claude Code.** Do the thinking in chat — plan, write the
|
||||
spec, make the decisions. Then hand that spec to a Claude Code agent that does the
|
||||
build: it implements, **verifies by running**, and pushes. The chat plans; the
|
||||
agent implements.
|
||||
|
||||
**tmux + Claude Code split.** In practice that's a Claude Code agent running in a
|
||||
tmux pane doing the build work while you plan/review in another. One spec in, a
|
||||
verified change out.
|
||||
|
||||
**claudedo (optional, hands-free).** Voice control for Claude Code over tmux: a
|
||||
wake-word plus local Whisper speech-to-text drives your tmux session without the
|
||||
keyboard — handy when you're fullscreen or away from the desk. It's available
|
||||
here:
|
||||
[git.rethinkstudios.io/rethink-software/claudedo](https://git.rethinkstudios.io/rethink-software/claudedo).
|
||||
|
||||
**The `.claude/` project convention.** Every project has a top-level `.claude/`
|
||||
folder (always gitignored — nothing under it is committed):
|
||||
|
||||
- **`CLAUDE.md`** — project-specific instructions for the agent (stack, layout,
|
||||
conventions). Layered on top of your global instructions.
|
||||
- **`compact.md`** — a running state log (done / decided / in-flight), updated at
|
||||
checkpoints so a fresh session catches up fast.
|
||||
- **`commands.log`** — an append-only record of shell commands run.
|
||||
- **`spec/`** — numbered, per-change specs named `NN-<type>-<short>.md`
|
||||
(e.g. `01-feature-logging.md`).
|
||||
|
||||
Tell the agent to **"setup project"** to scaffold all of that (and add `.claude/`
|
||||
to `.gitignore`) in a new repo.
|
||||
|
||||
**AI-assist habits.** When you're stuck, give the AI the *exact* context instead
|
||||
of describing it — the WSL clipboard bridge makes this trivial:
|
||||
|
||||
```bash
|
||||
git diff | clip.exe # then paste the diff straight into the chat
|
||||
```
|
||||
|
||||
Use AI for the plan and the spec, let the agent build, and always **prove it
|
||||
works by running it** — don't accept "this should work."
|
||||
|
||||
!!! tip "Verify by executing, not asserting"
|
||||
The single habit that separates good AI-assisted work from plausible-looking
|
||||
nonsense: **run it**. A passing build, a real screenshot, actual output — that
|
||||
is proof. "It should work" is not.
|
||||
|
||||
## Recommended setup
|
||||
|
||||
- **WSL2 (Ubuntu) + VS Code + Claude Code.** Develop in WSL, edit in VS Code over
|
||||
[Remote — WSL](https://code.visualstudio.com/docs/remote/wsl), and run
|
||||
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as your
|
||||
agent (terminal or the VS Code extension).
|
||||
- **[pyenv](https://github.com/pyenv/pyenv)** for per-project Python versions
|
||||
(we target **3.10+**) and isolated `.venv`s — full setup on the
|
||||
[Virtual environments](environments.md) page.
|
||||
- **[flake8](https://flake8.pycqa.org/)** with a shared config — max line length
|
||||
**120** (see the alias below).
|
||||
|
||||
## Handy shell setup
|
||||
|
||||
Copy-paste these into your `.zshrc` / `.bashrc`. Replace every placeholder with
|
||||
your real values. The high-value ones are explained underneath.
|
||||
|
||||
```bash
|
||||
# --- WSL <-> Windows bridges ---
|
||||
alias explorer="explorer.exe ." # open current dir in Windows Explorer
|
||||
# pipe to the Windows clipboard (great for pasting context into AI):
|
||||
# git diff | clip.exe -> paste the diff straight into a chat
|
||||
# auto-alias Windows .exe tools on PATH (e.g. adb/platform-tools):
|
||||
export PATH="$PATH:/mnt/c/<your>/platform-tools"
|
||||
for exe in /mnt/c/<your>/platform-tools/*.exe; do
|
||||
alias "$(basename "${exe}" .exe)"="${exe}"
|
||||
done
|
||||
|
||||
# --- pyenv (Python version management) ---
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
eval "$(pyenv init --path)"
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
|
||||
# --- flake8 with a shared config ---
|
||||
alias flake8='flake8 --config ~/.config/flake8'
|
||||
|
||||
# --- per-project git identity (switch identity per repo) ---
|
||||
# set the LOCAL (per-repo) user + the key to sign/push with:
|
||||
alias gitsetup='git config --local user.name "<you>"; \
|
||||
git config --local user.email "dev@<you>"; \
|
||||
git config --local core.sshCommand "ssh -i $HOME/.ssh/<key>"'
|
||||
# a second identity for a different account, same pattern:
|
||||
alias gitea='git config --local user.name "<alt>"; \
|
||||
git config --local user.email "<alt>@<host>"; \
|
||||
git config --local core.sshCommand "ssh -i $HOME/.ssh/<alt-key>"'
|
||||
|
||||
# --- git log graph ---
|
||||
alias gl='git log --graph --abbrev-commit --pretty=oneline --decorate'
|
||||
|
||||
# --- local bins on PATH ---
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
```
|
||||
|
||||
```bash
|
||||
# cherry-pick a commit onto master/main quickly
|
||||
gitcs() {
|
||||
if [ -z "$1" ]; then echo "Usage: gitcs <commit>"; return 1; fi
|
||||
git checkout master && git cherry-pick "$1"
|
||||
}
|
||||
```
|
||||
|
||||
**The ones worth understanding:**
|
||||
|
||||
- **`git diff | clip.exe`** — the killer WSL trick for AI-assisted dev. Pipe your
|
||||
working changes straight to the Windows clipboard and paste them into the chat
|
||||
so the AI sees *exactly* what changed instead of your paraphrase of it.
|
||||
- **`gitsetup` / `gitea`** — per-repo identity. One machine, multiple Gitea
|
||||
identities and keys; run the alias inside a repo to set its **local** user and
|
||||
signing/push key, so commits attribute correctly without touching your global
|
||||
config.
|
||||
- **`gl`** — a readable branch graph for understanding history at a glance.
|
||||
- **`pyenv`** — per-project Python versions, so each repo builds against the
|
||||
version it targets.
|
||||
26
mkdocs.yml
26
mkdocs.yml
@ -1,26 +1,20 @@
|
||||
site_name: Rethink Studios Handbook
|
||||
site_name: rethink development
|
||||
site_description: Libraries, coding standards, and how to deploy on our network.
|
||||
site_url: https://docs.rethinkstudios.io/
|
||||
copyright: Rethink Studios
|
||||
copyright: rethink development (handbook)
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
theme:
|
||||
name: material
|
||||
language: en
|
||||
logo: assets/logo.svg
|
||||
favicon: assets/logo.svg
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
primary: custom
|
||||
accent: custom
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
@ -59,4 +53,6 @@ nav:
|
||||
- Home: index.md
|
||||
- Libraries: libraries.md
|
||||
- Standards: standards.md
|
||||
- Workflow: workflow.md
|
||||
- Virtual environments: environments.md
|
||||
- Deploy: deploy.md
|
||||
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
# Build dependencies for the handbook docs site.
|
||||
# Host build: python -m venv .venv && . .venv/bin/activate
|
||||
# pip install -r requirements.txt && mkdocs build
|
||||
# Output: static site/ served by the reverse proxy at docs.rethinkstudios.io.
|
||||
|
||||
mkdocs-material==9.7.6 # theme — pulls mkdocs, pymdown-extensions, pygments
|
||||
mkdocs==1.6.1 # pinned explicitly for a reproducible build
|
||||
mkdocs-material-extensions==1.3.1
|
||||
pymdown-extensions==11.0 # admonitions, tabbed, superfences, annotations
|
||||
Pygments==2.20.0 # syntax highlighting in code blocks
|
||||
Loading…
Reference in New Issue
Block a user