The Deploy page got long and the code annotations rendered inconsistently
(some as (n) numbers, some as clickable +, and a marker on a fully-commented
line made that line vanish). Fix both:
- Deploy is now a HUB (docs/deploy/index.md): the intro + collision rule +
central-deploy note, then card links to three focused sub-pages. Deploy
stays the single global-nav entry; sub-pages are not_in_nav, reached from
the hub cards.
- deploy/compose.md — compose convention, storage tiers, how deploy fills
it in, subprocess/browser knobs, checklist
- deploy/dockerfile.md — services-account image, COPY, layer caching, uv
- deploy/secrets.md — keeping secrets out of the image
- Replace code annotations with INLINE COMMENTS on the compose/Dockerfile
examples: everything visible at once, no + to click, and the commented
MOUNTS_DIR line no longer disappears.
- Update inbound links (index card, standards, workflow, environments) to
deploy/ and deploy/compose.md; nav Deploy -> deploy/index.md with
not_in_nav for the sub-pages.
Verified in-browser: hub cards link correctly, sub-pages render with visible
inline comments (0 annotation markers), left nav shows only Deploy;
mkdocs build --strict clean (validates not_in_nav + all cross-links).
Signed-off-by: disqualifier <dev@disqualifier.me>
172 lines
5.3 KiB
Markdown
172 lines
5.3 KiB
Markdown
# Coding Standards
|
|
|
|
The house standards every Rethink Studios project follows. They keep our code
|
|
consistent, readable, and predictable across the whole suite.
|
|
|
|
## Files and style
|
|
|
|
- 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.
|
|
|
|
A well-formed module — module docstring, type hints, lowercase-start docstring:
|
|
|
|
```python
|
|
"""async key/value store backed by a single json file"""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
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.
|
|
|
|
!!! 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.
|
|
|
|
=== "Do"
|
|
|
|
```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:]
|
|
```
|
|
|
|
=== "Don't"
|
|
|
|
```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
|
|
```
|
|
|
|
## Quality and error handling
|
|
|
|
**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.
|
|
|
|
## Service compose
|
|
|
|
A deployable service ships a `compose.yaml` that names **nothing repo-specific** —
|
|
the deploy layer injects identity and host paths. See the
|
|
[Deploy guide](deploy/compose.md) for the full convention and the variables
|
|
you can rely on. The short version:
|
|
|
|
=== "Right"
|
|
|
|
```yaml
|
|
services:
|
|
svc:
|
|
user: "1337:1337"
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ${LOGS_DIR:-./logs}:/app/logs
|
|
- profile:/app/profile
|
|
|
|
volumes:
|
|
profile:
|
|
```
|
|
|
|
=== "Wrong (causes collisions)"
|
|
|
|
```yaml
|
|
services:
|
|
nova:
|
|
container_name: nova # repo-specific name -> collides
|
|
volumes:
|
|
- /srv/logs/ricky/nova:/app/logs # hardcoded host path
|
|
- nova_profile:/app/profile # repo-prefixed volume
|
|
|
|
volumes:
|
|
nova_profile:
|
|
```
|
|
|
|
Generic service key `svc`, no `container_name`, host paths from `${...}` variables,
|
|
and **bare** volume names — that's what makes a service collision-proof on the
|
|
fleet.
|