Skip to content

Commit 42f2507

Browse files
committed
Add Agents and fix lints
1 parent ef80b43 commit 42f2507

16 files changed

Lines changed: 1083 additions & 51 deletions

AGENTS.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# AGENTS.md — guide for AI agents working in this repository
2+
3+
If you are an AI coding agent (Claude Code, Codex, Cursor, GitHub Copilot Workspace, etc.) about to make changes to this repository, read this file first. It will save you from generating code that diverges from the codebase's conventions.
4+
5+
If you are a human contributor, the same conventions apply to you — but the more comprehensive [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) and the deep-dive references linked below are written for you specifically.
6+
7+
## What this repo is
8+
9+
`pytfe` is the official Python SDK for the HCP Terraform and Terraform Enterprise V2 API. It wraps roughly 50 resource services (workspaces, runs, policies, teams, agents, …) and is consumed by downstream projects. Source layout:
10+
11+
```
12+
src/pytfe/
13+
client.py # TFEClient — composition root, wires every resource
14+
config.py # TFEConfig — auth, timeout, retry, proxy settings
15+
_http.py # HTTPTransport — request, retry, redirects, auth
16+
_jsonapi.py # JSON:API envelope helpers
17+
errors.py # Typed exception hierarchy (TFEError + ~80 subclasses)
18+
utils.py # Validation + small helpers
19+
models/ # Pydantic v2 models, one file per resource
20+
resources/ # Service classes, one file per resource
21+
22+
tests/units/ # Pytest unit tests with mocked transport, one file per resource
23+
examples/ # Runnable CLI demos, one file per resource (or extended)
24+
docs/ # Internal reference (see below)
25+
```
26+
27+
## Required reading before generating code
28+
29+
These three documents define the patterns this codebase already uses. Generating code without consulting them will produce inconsistent output:
30+
31+
| Topic | Doc |
32+
|---|---|
33+
| `list_*` methods, pagination, iterator vs list, the `_list` helper | [`docs/ITERATORS.md`](docs/ITERATORS.md) |
34+
| Pydantic model conventions: `ConfigDict`, aliases, validators, relationships, exporting | [`docs/models.md`](docs/models.md) |
35+
| Resource service patterns: method shape, JSON:API envelopes, client wiring, examples | [`docs/resource.md`](docs/resource.md) |
36+
37+
Each doc ends with a checklist. Use those checklists; they encode the rules a reviewer will look for.
38+
39+
## Source verification for API shape
40+
41+
Before adding a new resource, endpoint, enum, or non-obvious response parser, verify the wire contract against primary sources:
42+
43+
- Official HCP Terraform API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs
44+
- go-tfe implementation: https://github.com/hashicorp/go-tfe
45+
- OpenAPI specs or live probes when the public docs/go-tfe are missing, beta, or ambiguous
46+
47+
Use the official docs and go-tfe as the first sources of truth. OpenAPI and live probes are supporting evidence, especially for endpoints that are newly released or not fully documented yet. When behavior is surprising, note the source you checked in the PR description, test name, example header, or a short code comment.
48+
49+
## The cardinal rules
50+
51+
A handful of conventions are pervasive enough that you'll regret breaking them. In rough order of "how loudly it breaks at review time":
52+
53+
1. **`list_*` methods return `Iterator[X]`.** Not `list[X]`, not `Iterable[X]`, not a custom `Pager`. Use `for item in self._list(path): yield ...` inside the method body. ([ITERATORS.md](docs/ITERATORS.md))
54+
55+
2. **JSON:API attribute names go through `Field(alias="...")`.** The API sends `created-at`, Python uses `created_at`. Pair with `model_config = ConfigDict(populate_by_name=True, validate_by_name=True)` on the model. ([models.md](docs/models.md))
56+
57+
3. **`model_dump(by_alias=True, exclude_none=True)` for write payloads.** Without `by_alias=True` you'll send snake_case to the API and it will silently drop the fields. Add `mode="json"` if the options contain enums.
58+
59+
4. **For new public APIs, prefer typed `TFEError` subclasses.** The error hierarchy in `errors.py` is part of the public API, and downstream consumers often `except TFEError:` once. Existing methods still expose many `ValueError` paths; do not change those established exceptions unless the breaking-change impact is explicitly accepted.
60+
61+
5. **Validate IDs at the top of every method.** Use `valid_string_id` from `utils.py`. New methods should prefer typed `Invalid<Thing>IDError` errors; existing resources may already use `ValueError` and should keep that public behavior unless a breaking change is intentional.
62+
63+
6. **Wire every new resource into `client.py`.** A resource not added to `TFEClient.__init__` is unreachable. Same for new models in `models/__init__.py`.
64+
65+
7. **Use the standard verb names: `list`, `read`, `create`, `update`, `delete`.** Plus `add_*` / `remove_*` for relationship modifications. Argument order is always *identifiers first, options last*.
66+
67+
## Things that look reasonable but are actually wrong here
68+
69+
These are mistakes a competent Python developer would make if they hadn't read the conventions. Avoid them:
70+
71+
- **Don't catch `httpx` errors directly.** The transport already translates them into `TFEError` subclasses. Catching `httpx.HTTPError` in a resource means the typed error never propagates.
72+
- **Don't send the TFE bearer token to presigned blob URLs.** Signed upload/download URLs carry their own credentials. For direct signed URLs, call `self.t.request(..., include_auth=False)`. For API endpoints that return a 3xx redirect to a signed blob, call with `allow_redirects=False`, read `Location`, then fetch that URL with `include_auth=False`.
73+
- **Don't write a custom page loop.** `self._list(path, params=...)` handles pagination + non-paginated endpoints transparently. Rolling your own loop will diverge from the rest of the codebase.
74+
- **Don't reuse generators.** Iterators returned by `list_*` are single-use. If you need to traverse twice, `materialized = list(client.foo.list_bars(...))` first.
75+
- **Don't add features beyond what was asked.** This codebase is approaching v1.0.0. Adding "while I'm here" refactors or speculative abstractions slows reviews and risks breaking the Ansible collection.
76+
- **Don't assume every successful response is `{"data": ...}`.** Check the docs/go-tfe/spec for each endpoint: some return a JSON:API envelope, some return a bare resource object, `204 No Content`, `null`, raw bytes, or a redirect to a blob URL. Add tests for non-standard shapes.
77+
- **Don't use bare `list[...]` annotations inside a resource class after defining `def list(...)`.** In class scope, mypy can resolve `list` to the method instead of the builtin. Use `builtins.list[...]`, `Sequence[...]`, or another unshadowed type.
78+
79+
## Known cross-dependencies you should not break
80+
81+
| Consumer | What they depend on |
82+
|---|---|
83+
| `hashicorp/terraform-ansible-collection` | The pytfe public API — resource methods, model fields, exception classes. Any signature change here is a breaking change. In particular, `client.projects.list_tag_bindings` is consumed with an `isinstance(response, list)` check; it intentionally still returns `list[TagBinding]` (see [ITERATORS.md](docs/ITERATORS.md) — Known exceptions). |
84+
| Downstream user code generally | Method signatures, return types, model fields, and exception types. New errors should subclass an existing parent so `except TFEError:` continues to work, but existing `ValueError` behavior should not be changed casually. |
85+
86+
When in doubt about whether a change is breaking: check `gh search code '<symbol>' --owner hashicorp` to see if the Ansible repo uses it.
87+
88+
## How to make a change
89+
90+
This is the workflow that produces low-friction reviews. Follow it.
91+
92+
1. **Understand the scope first.** If the task is "add resource X", read `docs/resource.md` end-to-end. If it's "fix bug in Y", read `Y`'s current implementation and tests before touching anything.
93+
2. **Check official API docs and go-tfe for the canonical API shape.** `pytfe` mirrors the HCP Terraform API and often follows go-tfe's surface. URL paths, method names, payload shapes, response shapes, enum values, and redirect behavior should be verified against https://developer.hashicorp.com/terraform/cloud-docs/api-docs and https://github.com/hashicorp/go-tfe before designing anything.
94+
3. **Add models first** (`src/pytfe/models/<resource>.py`), then the resource (`src/pytfe/resources/<resource>.py`), then wire both into the respective `__init__.py` / `client.py`.
95+
4. **Write tests.** Mock `HTTPTransport`. One test per method, plus an invalid-id case for every public method. See `tests/units/test_comment.py` as a small reference.
96+
5. **Run `make test` and `make lint`.** Both must pass. `pytest tests/units/` runs the suite directly; it should be < 2 seconds.
97+
6. **Add or extend an example.** Real engineers will copy-paste it; make it work end-to-end. Use env vars (`TFE_TOKEN`, `TFE_ORG`) for auth, never hard-code credentials.
98+
7. **If the change is non-trivial, verify live.** The repo doesn't run integration tests in CI, so the only way to catch a wrong URL or a typo in an attribute alias is to run the example against a real organization.
99+
100+
## Things to never do
101+
102+
- **Never put a token, password, or other credential in any file.** Use environment variables. The user will rotate them after; you don't need to know them.
103+
- **Never use `git push --force` or `git reset --hard` without explicit instruction.** Same for `--no-verify`, force-push to `main`, or rebasing public commits.
104+
- **Never commit `.env`, `credentials.json`, `*.tfstate`, or anything with secrets.** Match against the existing `.gitignore` if unsure.
105+
- **Never bypass pre-commit hooks.** If a hook fails, fix the underlying issue.
106+
- **Never run an example that creates real resources against production without explicit user confirmation.** Sandbox orgs are safe; user's actual workspace is not.
107+
108+
## Style
109+
110+
The codebase uses [ruff](https://docs.astral.sh/ruff/) for both formatting and linting and [mypy](https://mypy.readthedocs.io/) for type checking. Type hints are required on every public method's signature. Docstrings are required on every public method — keep them to one or two lines unless the behavior is genuinely non-obvious.
111+
112+
Comments are minimal by design. A comment should explain *why* something non-obvious is true, not *what* the code does. The names and types should be enough to convey "what".
113+
114+
```python
115+
# ❌ Don't
116+
# Increment the counter by 1
117+
counter += 1
118+
119+
# ✅ Do (only when the why is non-obvious)
120+
# Run task stages are wire values, not Python names. If the API/go-tfe says
121+
# "pre-plan", keep the hyphen; do not "normalize" it to snake_case.
122+
stage_value = raw_value
123+
```
124+
125+
## When you're done
126+
127+
A reasonable PR includes:
128+
129+
- Code (resource + models)
130+
- Tests covering every public method
131+
- An updated or new example
132+
- A short `CHANGELOG.md` entry under `# v<next-minor>.0 (Unreleased)` describing the user-visible change
133+
134+
Open the PR with a description that explains *why* the change is needed, links to any HCP Terraform API docs or go-tfe code referenced, and notes any behavior changes a downstream consumer might see.
135+
136+
The reviewer's checklist will be the union of the checklists in [`docs/ITERATORS.md`](docs/ITERATORS.md), [`docs/models.md`](docs/models.md), and [`docs/resource.md`](docs/resource.md). Pre-running them yourself is the fastest way to a merge.

0 commit comments

Comments
 (0)