Skip to content

Commit a4eb6de

Browse files
claudeonefloid
authored andcommitted
Add CLAUDE.md documenting codebase structure and conventions
Covers the Typer/TM1py command architecture, database config resolution (databases.yaml vs TM1_* env vars), testing patterns (mocked vs live-server tests), linting setup, and the release workflow so AI assistants can navigate and extend the CLI consistently.
1 parent e9e8d67 commit a4eb6de

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# CLAUDE.md
2+
3+
Guidance for AI assistants (Claude Code) working in this repository.
4+
5+
## What this project is
6+
7+
**tm1cli** is a Python command-line interface for interacting with IBM Planning
8+
Analytics / TM1 servers, built on top of [TM1py](https://github.com/cubewise-code/tm1py)
9+
and [Typer](https://typer.tiangolo.com/). It's a small, published PyPI package
10+
(`pip install tm1cli`), currently at version `0.1.7` (see `pyproject.toml`).
11+
12+
## Tech stack
13+
14+
- Python `^3.10`
15+
- **Typer** — CLI framework (commands are plain functions decorated with `@app.command()`)
16+
- **TM1py** — TM1 REST API client, used inside a `with TM1Service(...) as tm1:` context manager
17+
- **Rich** — colored/table console output (`rich.print`, `rich.console.Console`, `rich.table.Table`)
18+
- **PyYAML** — for `databases.yaml` config and process dump/load YAML serialization
19+
- **Poetry** — dependency management and packaging (`poetry-core` build backend)
20+
- Dev tools: **pytest** + **pytest-mock**, **pylint**, **isort**
21+
22+
## Repository layout
23+
24+
```
25+
tm1cli/
26+
main.py # Typer app entrypoint; top-level commands (tm1-version, whoami, threads, version)
27+
# and database config resolution (databases.yaml vs TM1_* env vars)
28+
commands/
29+
__init__.py # re-exports all command submodules
30+
cube.py # `tm1cli cube ...`
31+
dimension.py # `tm1cli dimension ...`
32+
process.py # `tm1cli process ...` (list/exists/clone/dump/load)
33+
subset.py # `tm1cli subset ...`
34+
view.py # `tm1cli view ...`
35+
utils/
36+
cli_param.py # shared typer.Option definitions (DATABASE_OPTION, WATCH_OPTION, INTERVAL_OPTION)
37+
various.py # resolve_database(), print_error_and_exit()
38+
watch.py # @watch_option decorator for polling commands (--watch/-w, --interval)
39+
tm1yaml.py # custom YAML dump/load for TM1py Process objects (multi-line script sections)
40+
tests/
41+
conftest.py # Mocked*Service classes + MockedTM1Service used to stub TM1py in tests
42+
test_tm1cli.py # tests for top-level commands (tm1-version, whoami, threads, process ...)
43+
test_cmd_cubes.py # cube command tests (uses mocker.patch to swap in MockedTM1Service)
44+
test_cmd_dimension.py
45+
test_cmd_subset.py
46+
test_cmd_view.py
47+
databases.yaml.template # example multi-database config; real databases.yaml is gitignored
48+
pyproject.toml # Poetry config, dependencies, `tm1cli` console script entry point
49+
requirements.txt # generated/plain pip requirements (kept in sync manually, used by pylint CI)
50+
.pylintrc # pylint config
51+
.github/workflows/
52+
pylint.yml # runs pylint on tm1cli/**/*.py for Python 3.10 and 3.11 on every push
53+
pypi-publish.yml # builds and publishes to PyPI on GitHub release
54+
CHANGELOG.md # manually maintained, one section per version
55+
```
56+
57+
## Command architecture pattern
58+
59+
Each resource (cube, dimension, process, subset, view) has its own Typer
60+
sub-app in `tm1cli/commands/<name>.py`, registered in `main.py` via:
61+
62+
```python
63+
modules = [("process", commands.process), ("cube", commands.cube), ...]
64+
for name, module in modules:
65+
app.add_typer(module.app, name=name)
66+
```
67+
68+
A typical command function follows this shape:
69+
70+
```python
71+
@app.command()
72+
@watch_option
73+
def exists(
74+
ctx: typer.Context,
75+
cube_name: str,
76+
database: Annotated[str, DATABASE_OPTION] = None,
77+
watch: Annotated[bool, WATCH_OPTION] = False, # pylint: disable=unused-argument
78+
interval: Annotated[int, INTERVAL_OPTION] = 5, # pylint: disable=unused-argument
79+
):
80+
"""Check if cube exists"""
81+
with TM1Service(**resolve_database(ctx, database)) as tm1:
82+
print(tm1.cubes.exists(cube_name))
83+
```
84+
85+
Conventions to follow when adding or modifying commands:
86+
87+
- Always open the TM1py client as `with TM1Service(**resolve_database(ctx, database)) as tm1:`
88+
never construct `TM1Service` outside a context manager.
89+
- Every command accepts an optional `database: Annotated[str, DATABASE_OPTION] = None` parameter
90+
and resolves it via `resolve_database(ctx, database)`.
91+
- `list` commands are also registered under the alias `ls` via a second
92+
`@app.command(name="ls", help="Alias for list")` decorator stacked above `@app.command(name="list")`.
93+
Follow this same double-decorator pattern for other aliases (e.g. `export`/`dump`, `import`/`load`
94+
in `process.py`).
95+
- Commands that make sense to poll (existence checks) are decorated with `@watch_option` from
96+
`tm1cli/utils/watch.py` and take `watch`/`interval` parameters — mark them
97+
`# pylint: disable=unused-argument` since the decorator consumes them via `**kwargs`, not the
98+
function body.
99+
- Use `rich.print` (imported `# pylint: disable=redefined-builtin`) for normal output, and
100+
`print_error_and_exit(msg)` from `tm1cli/utils/various.py` for user-facing errors — it prints in
101+
bold red and raises `typer.Exit(code=1)`. Don't raise raw exceptions for expected error paths.
102+
- Reuse `DATABASE_OPTION`, `WATCH_OPTION`, `INTERVAL_OPTION` from `tm1cli/utils/cli_param.py` instead
103+
of redefining `typer.Option(...)` inline.
104+
105+
## Database configuration resolution
106+
107+
`main.py`'s `@app.callback()` builds `ctx.obj` once per invocation:
108+
109+
- If `databases.yaml` exists in the current working directory, it's parsed into
110+
`ctx.obj["configs"]` (keyed by database `name`) and `ctx.obj["default_db_config"]` (first entry).
111+
- Otherwise, environment variables prefixed `TM1_` (e.g. `TM1_ADDRESS`, `TM1_PORT`, `TM1_SSL`,
112+
`TM1_USER`, `TM1_PASSWORD`) are lower-cased and stripped of the prefix to build
113+
`default_db_config`; `configs` is empty in this mode.
114+
- `resolve_database(ctx, database_name)` in `tm1cli/utils/various.py` returns
115+
`default_db_config` when no `--database`/`-d` is given, otherwise looks up `database_name` in
116+
`configs` and calls `print_error_and_exit` if not found.
117+
- The `--database`/`-d` option and `process clone --from/--to` **only work with a `databases.yaml`
118+
file** (env-var mode has no named configs) — preserve this constraint if touching this logic.
119+
120+
## Testing
121+
122+
- Test framework: `pytest` with `typer.testing.CliRunner` (`runner.invoke(app, [...])`) and
123+
`pytest-mock`'s `mocker` fixture.
124+
- Two testing styles exist side by side:
125+
- `test_cmd_*.py` files mock TM1py entirely via `mocker.patch("tm1cli.commands.<module>.TM1Service", MockedTM1Service)`
126+
using the fake service classes in `tests/conftest.py` (`MockedCubeService`, `MockedViewService`,
127+
`MockedDimensionService`, `MockedSubsetService`, `MockedProcessService`, `MockedTM1Service`).
128+
These run without any real TM1 server and are the pattern to follow for new command tests.
129+
- `test_tm1cli.py` exercises top-level commands (`tm1-version`, `whoami`, `threads`, `process ...`)
130+
**without mocking TM1Service**, so it requires a real, reachable TM1 server configured via
131+
`databases.yaml` or `TM1_*` env vars (with a `mydb`/`remotedb` style config for the
132+
`--database`/`-d` parametrized cases). Don't assume these pass in a sandboxed environment with
133+
no TM1 server available.
134+
- Several test functions in `test_tm1cli.py` are intentionally empty stubs (`def test_...(): ...`)
135+
— placeholders for not-yet-implemented coverage (e.g. `test_process_clone_sucess`,
136+
`test_process_dump_invalid_format`). Don't delete them without reason; filling them in is welcome.
137+
- Run tests with `pytest` (mocked command tests only, if no TM1 server is available, target them
138+
explicitly, e.g. `pytest tests/test_cmd_cubes.py tests/test_cmd_dimension.py tests/test_cmd_subset.py tests/test_cmd_view.py`).
139+
- There is no test job in CI today — `.github/workflows/pylint.yml` only runs pylint on `tm1cli/**/*.py`
140+
(Python 3.10 and 3.11); pytest is not invoked by GitHub Actions.
141+
142+
## Linting
143+
144+
- `pylint` is configured via `.pylintrc`: `max-line-length=120`, and
145+
`missing-module-docstring` / `missing-class-docstring` / `missing-function-docstring` /
146+
`too-many-arguments` / `too-many-positional-arguments` are disabled project-wide.
147+
- Command docstrings are still used deliberately as Typer `--help` text (e.g. `"""List cubes"""`),
148+
so keep writing them even though pylint doesn't require them.
149+
- CI runs `pylint $(git ls-files 'tm1cli/**/*.py')` — only the `tm1cli/` package is linted, not `tests/`.
150+
- `isort` is a listed dev dependency for import ordering; keep imports grouped stdlib / third-party /
151+
local (`tm1cli...`), matching the existing files.
152+
153+
## Adding a new resource/command
154+
155+
1. Create `tm1cli/commands/<resource>.py` with its own `app = typer.Typer()`, following the pattern
156+
in an existing file (e.g. `cube.py` for a simple list/exists resource, `process.py` for a
157+
resource with more operations).
158+
2. Add the import and `__all__` entry in `tm1cli/commands/__init__.py`.
159+
3. Register it in the `modules` list in `tm1cli/main.py`.
160+
4. Add mocked-service tests in `tests/test_cmd_<resource>.py` plus a corresponding
161+
`Mocked<Resource>Service` class in `tests/conftest.py`, mirroring the existing ones.
162+
5. Add usage examples to the README's command list and a `CHANGELOG.md` entry under a new version
163+
heading (features / fixes / chore / docs sections, matching the existing style).
164+
165+
## Releasing
166+
167+
- Bump `version` in `pyproject.toml`.
168+
- Add a new dated entry at the top of `CHANGELOG.md` (format: `## vX.Y.Z - YYYY-MM-DD` with
169+
`### Features` / `### Fix` / `### Chore` / `### Docs` / `### Breaking changes` subsections as needed).
170+
- Publishing to PyPI happens via `.github/workflows/pypi-publish.yml`, triggered by creating a
171+
GitHub release (or manual `workflow_dispatch`) — it builds with `python -m build` and uses
172+
PyPI trusted publishing (no manual token needed).
173+
174+
## Things to watch out for
175+
176+
- `requirements.txt` is a separate, manually-tracked file from `poetry.lock`/`pyproject.toml` and is
177+
what the pylint CI workflow installs from — if you add/change a dependency in `pyproject.toml`,
178+
update `requirements.txt` too (or regenerate it) so CI installs the same set.
179+
- `databases.yaml` is gitignored — never commit real credentials; use `databases.yaml.template` as
180+
the checked-in example and keep it in sync with any config schema changes.
181+
- The custom YAML process (de)serialization in `tm1cli/utils/tm1yaml.py` renders TM1 process script
182+
sections (`PrologProcedure`, `MetadataProcedure`, `DataProcedure`, `EpilogProcedure`) as multi-line
183+
YAML block scalars specifically so process dumps are readable/diffable in git — preserve this
184+
behavior if touching `dump`/`load`.

0 commit comments

Comments
 (0)