Contributing to insto¶
Thanks for considering a contribution. This document covers the dev workflow.
Setup¶
Python 3.11+ required (we use dataclass(slots=True), X | Y unions, datetime.fromisoformat with Z).
Running tests¶
uv run pytest # all tests
uv run pytest -k hiker # subset
uv run pytest --cov=insto --cov-report=term-missing
The suite is fully offline — no real HikerAPI or Instagram calls. There's also a structured live smoke against the real HikerAPI:
Eight REQ checks (resolve / profile / posts / followers / tagged / hashtag / quota / 404) plus one OPT check (/similar, per-target flaky). Skips with exit 0 if HIKERAPI_TOKEN_TEST is unset, so it's safe to wire into release-prep gates. Costs ~10 requests, single-digit cents. Run before each release tag — caught a real iter_hashtag_posts bug that mocks couldn't.
Coverage targets: keep pure-logic modules at 100% (models, _redact, exceptions, mappers). Everything else: 90%+ on touched code.
Lint + types¶
CI runs the same three; PRs blocked on the strict mypy gate.
Commit conventions¶
We use Conventional Commits. Allowed types:
feat:— new user-visible behavior (changelog "Added")fix:— bug fix (changelog "Fixed")perf:— performance only (changelog "Performance")refactor:— restructure without behavior change (changelog "Changed")docs:— documentation onlybuild:/ci:/chore:/test:/style:— hidden from changelog
PR titles follow the same shape and are CI-checked. release-please assembles CHANGELOG.md and bumps the version automatically when commits land on main.
Project layout¶
insto/
├── _redact.py # secret redaction (used everywhere we output)
├── _version.py # single source of version truth
├── cli.py # one-shot CLI + setup wizard + _format_error
├── repl.py # prompt_toolkit REPL + slash completer
├── config.py # config precedence: flag > env > toml
├── exceptions.py # backend error taxonomy
├── models.py # DTOs (Profile, Post, Story, Quota, ...)
├── ui/ # banner, theme, render helpers
├── backends/ # OSINTBackend ABC, HikerBackend, _retry, _cdn
├── service/ # facade, history, analytics, exporter, watch
└── commands/ # one file per group (target/profile/media/...)
tests/
├── fakes.py # FakeBackend with per-method error injection
├── fixtures/hiker/ # frozen HikerAPI dict responses per access state
├── e2e/ # subprocess + prompt_toolkit pty tests
└── test_*.py # one per module
Adding a command¶
- Pick the right module under
insto/commands/. - Use
@command("name", "help text", csv=..., add_args=...)from_base.py. - Decorate with
@with_target(gives youusername: str) or@with_pk(gives youpk: str). - To accept inline target on the command line (
/info instagram), passadd_args=add_target_arg(or compose with your own). - Return the value the REPL should echo; the dispatcher handles JSON / CSV / Maltego export.
Add tests in the matching tests/test_commands_*.py.
Adding a backend¶
- New file under
insto/backends/. - Subclass
OSINTBackendfrom_base.py. Implement every abstract async iterator +resolve_target+get_quota. - Wrap SDK calls in
@self._apply_retrysoRateLimited/Transientare retried; surface domain errors from the taxonomy inexceptions.py(no naked SDK exceptions above the backend layer). - Register in
backends/__init__.py:make_backend()with a lazy import — the SDK gets imported only when the user picks that backend.
Releasing¶
Maintainer-only.
- Land Conventional Commits on
main. - release-please opens a release PR with a
CHANGELOG.mdbump + version bump. - Merge the release PR. release-please tags
vX.Y.Z. - The
release.ymlworkflow picks up the tag, builds, and publishes to PyPI via trusted-publishing.