paranoid-atlas-cli 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ name: Publish paranoid-atlas-cli to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.11"
23
+
24
+ - name: Check the tag matches the package version
25
+ if: startsWith(github.ref, 'refs/tags/v')
26
+ run: |
27
+ version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
28
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
29
+ echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $version"
30
+ exit 1
31
+ fi
32
+
33
+ - name: Build
34
+ run: |
35
+ python -m pip install --upgrade pip build twine
36
+ python -m build
37
+ twine check dist/*
38
+
39
+ - name: Publish to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
41
+ with:
42
+ username: __token__
43
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ .pytest_cache/
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.5
2
+ Name: paranoid-atlas-cli
3
+ Version: 0.1.1
4
+ Summary: The Atlas method made executable: create and keep Atlases in order
5
+ Project-URL: Homepage, https://atlas.paranoid.software
6
+ Project-URL: Repository, https://github.com/paranoid-software/atlas-cli
7
+ Project-URL: Issues, https://github.com/paranoid-software/atlas-cli/issues
8
+ Author: Paranoid Software
9
+ License-Expression: MIT
10
+ Keywords: agents,atlas,claude,cli,spec-driven
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+
22
+ # atlas-cli
23
+
24
+ The [Atlas](https://atlas.paranoid.software) method made executable: create an Atlas and keep
25
+ it in order while several people and agents work in it.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ uv tool install paranoid-atlas-cli
31
+ ```
32
+
33
+ ## Develop
34
+
35
+ ```bash
36
+ uv sync
37
+ uv run atlas --version
38
+ uv run pytest
39
+ ```
40
+
41
+ ## Release
42
+
43
+ Bump `version` in `pyproject.toml`, commit, then push a matching tag (`v0.1.1` for `0.1.1`):
44
+ the `Publish paranoid-atlas-cli to PyPI` workflow builds and publishes it. A tag that doesn't match the
45
+ version fails the workflow.
@@ -0,0 +1,24 @@
1
+ # atlas-cli
2
+
3
+ The [Atlas](https://atlas.paranoid.software) method made executable: create an Atlas and keep
4
+ it in order while several people and agents work in it.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ uv tool install paranoid-atlas-cli
10
+ ```
11
+
12
+ ## Develop
13
+
14
+ ```bash
15
+ uv sync
16
+ uv run atlas --version
17
+ uv run pytest
18
+ ```
19
+
20
+ ## Release
21
+
22
+ Bump `version` in `pyproject.toml`, commit, then push a matching tag (`v0.1.1` for `0.1.1`):
23
+ the `Publish paranoid-atlas-cli to PyPI` workflow builds and publishes it. A tag that doesn't match the
24
+ version fails the workflow.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "paranoid-atlas-cli"
7
+ version = "0.1.1"
8
+ description = "The Atlas method made executable: create and keep Atlases in order"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [
12
+ { name = "Paranoid Software" },
13
+ ]
14
+ license = "MIT"
15
+ keywords = ["atlas", "cli", "agents", "spec-driven", "claude"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Build Tools",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.urls]
29
+ Homepage = "https://atlas.paranoid.software"
30
+ Repository = "https://github.com/paranoid-software/atlas-cli"
31
+ Issues = "https://github.com/paranoid-software/atlas-cli/issues"
32
+
33
+ [project.scripts]
34
+ atlas = "atlas_cli.cli:main"
35
+
36
+ [dependency-groups]
37
+ dev = ["pytest>=8"]
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/atlas_cli"]
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("paranoid-atlas-cli")
@@ -0,0 +1,29 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from atlas_cli import __version__
5
+ from atlas_cli.commands import init
6
+
7
+
8
+ def _build_parser() -> argparse.ArgumentParser:
9
+ parser = argparse.ArgumentParser(
10
+ prog="atlas",
11
+ description="Create and keep Atlases in order — https://atlas.paranoid.software",
12
+ )
13
+ parser.add_argument("--version", action="version", version=f"atlas {__version__}")
14
+ subparsers = parser.add_subparsers(dest="command", metavar="<command>")
15
+ init.register(subparsers)
16
+ return parser
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = _build_parser()
21
+ args = parser.parse_args(argv)
22
+ if args.command is None:
23
+ parser.print_help()
24
+ return 0
25
+ return args.handler(args)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ sys.exit(main())
@@ -0,0 +1,97 @@
1
+ import argparse
2
+ import json
3
+ from datetime import date
4
+ from importlib.resources import files
5
+ from pathlib import Path
6
+ from string import Template
7
+
8
+ ATLAS_FILES = {
9
+ "CLAUDE.md": "CLAUDE.md",
10
+ "STATUS.md": "STATUS.md",
11
+ "BACKLOG.md": "BACKLOG.md",
12
+ "_archived/README.md": "archived.md",
13
+ "_settings/atlas.toml": "atlas.toml",
14
+ }
15
+ CLAUDE_SETTINGS = ".claude/settings.local.json"
16
+ VSCODE_SETTINGS = ".vscode/settings.json"
17
+ GITIGNORE = ".gitignore"
18
+ ATLAS_MARKERS = ("_settings/atlas.toml", "CLAUDE.md", "STATUS.md")
19
+
20
+
21
+ def register(subparsers) -> None:
22
+ parser = subparsers.add_parser("init", help="create an Atlas in the current folder, named after it")
23
+ parser.set_defaults(handler=run)
24
+
25
+
26
+ def run(args: argparse.Namespace) -> int:
27
+ folder = Path.cwd()
28
+ found = [marker for marker in ATLAS_MARKERS if (folder / marker).exists()]
29
+ if found:
30
+ print(f"This folder is already an Atlas (found {', '.join(found)}); nothing was changed.")
31
+ if "_settings/atlas.toml" not in found:
32
+ print("It predates _settings/ — bring it up to date with `atlas migrate`.")
33
+ return 1
34
+ try:
35
+ plan = _plan_base_set(folder, date.today())
36
+ except ValueError as error:
37
+ print(f"{error}; nothing was changed.")
38
+ return 1
39
+ for path, (action, content) in plan.items():
40
+ if content is not None:
41
+ _write(folder / path, content)
42
+ print(f"{action} {path}")
43
+ if plan[GITIGNORE][0] == "kept":
44
+ print(f"Check that the existing {GITIGNORE} keeps only the Atlas's own files (see 07-sharing-an-atlas).")
45
+ print("Next: add each source with `atlas link <name> <path>`.")
46
+ return 0
47
+
48
+
49
+ def _plan_base_set(folder: Path, today: date) -> dict[str, tuple[str, str | None]]:
50
+ plan = {
51
+ path: ("created", _render_template(template, name=folder.name, today=today.isoformat()))
52
+ for path, template in ATLAS_FILES.items()
53
+ }
54
+ plan[CLAUDE_SETTINGS] = _plan_json(folder / CLAUDE_SETTINGS, _complete_claude_settings)
55
+ plan[VSCODE_SETTINGS] = _plan_json(folder / VSCODE_SETTINGS, _complete_vscode_settings)
56
+ if (folder / GITIGNORE).exists():
57
+ plan[GITIGNORE] = ("kept", None)
58
+ else:
59
+ plan[GITIGNORE] = ("created", _render_template("gitignore"))
60
+ return plan
61
+
62
+
63
+ def _plan_json(target: Path, complete) -> tuple[str, str]:
64
+ if not target.exists():
65
+ return "created", _dump_json(complete({}))
66
+ try:
67
+ settings = json.loads(target.read_text(encoding="utf-8"))
68
+ except json.JSONDecodeError as error:
69
+ raise ValueError(f"Cannot read {target.name} in {target.parent.name}/ ({error.msg})") from error
70
+ return "updated", _dump_json(complete(settings))
71
+
72
+
73
+ def _complete_claude_settings(settings: dict) -> dict:
74
+ permissions = settings.setdefault("permissions", {})
75
+ permissions.setdefault("allow", [])
76
+ permissions.setdefault("additionalDirectories", [])
77
+ return settings
78
+
79
+
80
+ def _complete_vscode_settings(settings: dict) -> dict:
81
+ settings["git.autoRepositoryDetection"] = True
82
+ settings.setdefault("git.scanRepositories", [])
83
+ return settings
84
+
85
+
86
+ def _dump_json(settings: dict) -> str:
87
+ return json.dumps(settings, indent=2, ensure_ascii=False) + "\n"
88
+
89
+
90
+ def _render_template(template: str, **values: str) -> str:
91
+ text = files("atlas_cli.templates").joinpath(template).read_text(encoding="utf-8")
92
+ return Template(text).safe_substitute(values)
93
+
94
+
95
+ def _write(target: Path, content: str) -> None:
96
+ target.parent.mkdir(parents=True, exist_ok=True)
97
+ target.write_text(content, encoding="utf-8", newline="\n")
@@ -0,0 +1,7 @@
1
+ # $name — BACKLOG
2
+
3
+ The queue: one line per open workitem (`SPEC_*` / `DRAFT_*`) not yet in flight. Index only —
4
+ no bodies, no rules, no decisions.
5
+
6
+ ## Open
7
+ _(empty)_
@@ -0,0 +1,36 @@
1
+ # $name Atlas — agent instructions
2
+
3
+ This is the **$name Atlas** — the context of the $name project. Project orientation +
4
+ Atlas-specific glue only. Universal rules and behavior live in the memory store; autonomous
5
+ file-memory is off. **Current state & next steps live in [STATUS.md](STATUS.md) — read it
6
+ first.**
7
+
8
+ ---
9
+
10
+ ## 1. What lives here — sources and their purposes
11
+
12
+ Each source is a symlink; where it lives is up to each machine and is never recorded here.
13
+
14
+ _(no sources yet — add each one with `atlas link <name> <path>`, then its block below)_
15
+
16
+ Editing a file under a symlink writes to the source itself. Changes across sources are
17
+ flagged explicitly.
18
+
19
+ ---
20
+
21
+ ## 2. How I work in this Atlas
22
+ - **Before editing, identify the target source** — most changes belong to exactly one;
23
+ changes across sources are called out explicitly.
24
+
25
+ ---
26
+
27
+ ## 3. $name — product / architecture
28
+ _(no domain conventions yet)_
29
+
30
+ ---
31
+
32
+ ## 5. Conventions we're establishing (Atlas-level, not in the memory store)
33
+ _Rules that apply to this Atlas and that we've decided not (yet) to lift into the memory
34
+ store. Keep it short; promote to the memory store only after a dedicated session, and only
35
+ if they generalize._
36
+ - _(empty — fill in as conventions get decided.)_
@@ -0,0 +1,15 @@
1
+ # $name — STATUS (living)
2
+
3
+ Where this project is now. Standing decisions + orientation live in CLAUDE.md; the
4
+ BACKLOG / SPEC_ / _archived artifacts are the record. Refresh with `/atlas-sync`.
5
+
6
+ _Last updated: ${today}_
7
+
8
+ ## What $name is (one line)
9
+ _(one-line summary)_
10
+
11
+ ## Active
12
+ _(one block per SPEC in progress / in review — rewritten to the current state, never appended)_
13
+
14
+ ## Recently closed
15
+ _(short rolling window — last N — pointing into _archived/; the full history lives there)_
@@ -0,0 +1,3 @@
1
+ # _archived
2
+
3
+ Holds only closed `SPEC_NNNN_<SLUG>.md` files and their `SPEC_NNNN_FINDINGS.md` — the project's history of work.
@@ -0,0 +1,3 @@
1
+ version = 1
2
+
3
+ [plugins]
@@ -0,0 +1,5 @@
1
+ /*
2
+ !/*.md
3
+ !/_*/
4
+ !/.vscode/
5
+ !/.gitignore
@@ -0,0 +1,11 @@
1
+ import pytest
2
+
3
+ from atlas_cli import __version__
4
+ from atlas_cli.cli import main
5
+
6
+
7
+ def test_print_the_installed_version(capsys):
8
+ with pytest.raises(SystemExit) as exit_info:
9
+ main(["--version"])
10
+ assert exit_info.value.code == 0
11
+ assert capsys.readouterr().out.strip() == f"atlas {__version__}"
@@ -0,0 +1,182 @@
1
+ import json
2
+ import tomllib
3
+ from datetime import date
4
+ from pathlib import Path
5
+
6
+ from atlas_cli.cli import main
7
+
8
+
9
+ def _files_under(folder: Path) -> set[str]:
10
+ return {str(path.relative_to(folder)) for path in folder.rglob("*") if path.is_file()}
11
+
12
+
13
+ def test_create_the_base_set_and_nothing_else(tmp_path, monkeypatch):
14
+ monkeypatch.chdir(tmp_path)
15
+
16
+ assert main(["init"]) == 0
17
+
18
+ assert _files_under(tmp_path) == {
19
+ "CLAUDE.md",
20
+ "STATUS.md",
21
+ "BACKLOG.md",
22
+ "_archived/README.md",
23
+ "_settings/atlas.toml",
24
+ ".claude/settings.local.json",
25
+ ".vscode/settings.json",
26
+ ".gitignore",
27
+ }
28
+
29
+
30
+ def test_start_with_no_sources_and_no_plugins(tmp_path, monkeypatch):
31
+ monkeypatch.chdir(tmp_path)
32
+
33
+ main(["init"])
34
+
35
+ settings = tomllib.loads((tmp_path / "_settings/atlas.toml").read_text(encoding="utf-8"))
36
+ assert settings == {"version": 1, "plugins": {}}
37
+ claude = json.loads((tmp_path / ".claude/settings.local.json").read_text(encoding="utf-8"))
38
+ assert claude == {"permissions": {"allow": [], "additionalDirectories": []}}
39
+ vscode = json.loads((tmp_path / ".vscode/settings.json").read_text(encoding="utf-8"))
40
+ assert vscode == {"git.autoRepositoryDetection": True, "git.scanRepositories": []}
41
+
42
+
43
+ def test_ignore_everything_but_the_atlas_files(tmp_path, monkeypatch):
44
+ monkeypatch.chdir(tmp_path)
45
+
46
+ main(["init"])
47
+
48
+ assert (tmp_path / ".gitignore").read_text(encoding="utf-8").splitlines() == [
49
+ "/*",
50
+ "!/*.md",
51
+ "!/_*/",
52
+ "!/.vscode/",
53
+ "!/.gitignore",
54
+ ]
55
+
56
+
57
+ def test_name_the_atlas_after_its_folder(tmp_path, monkeypatch):
58
+ folder = tmp_path / "log-daemon"
59
+ folder.mkdir()
60
+ monkeypatch.chdir(folder)
61
+
62
+ main(["init"])
63
+
64
+ assert (folder / "CLAUDE.md").read_text(encoding="utf-8").startswith("# log-daemon Atlas")
65
+ assert (folder / "STATUS.md").read_text(encoding="utf-8").startswith("# log-daemon — STATUS")
66
+
67
+
68
+ def test_date_the_status_with_today(tmp_path, monkeypatch):
69
+ monkeypatch.chdir(tmp_path)
70
+
71
+ main(["init"])
72
+
73
+ assert f"_Last updated: {date.today().isoformat()}_" in (tmp_path / "STATUS.md").read_text(encoding="utf-8")
74
+
75
+
76
+ def test_write_the_same_files_on_every_run(tmp_path, monkeypatch):
77
+ first, second = tmp_path / "acme", tmp_path / "other" / "acme"
78
+ for folder in (first, second):
79
+ folder.mkdir(parents=True)
80
+ monkeypatch.chdir(folder)
81
+ main(["init"])
82
+
83
+ for path in _files_under(first):
84
+ assert (first / path).read_bytes() == (second / path).read_bytes(), path
85
+
86
+
87
+ def test_write_utf8_with_unix_line_endings(tmp_path, monkeypatch):
88
+ folder = tmp_path / "señal"
89
+ folder.mkdir()
90
+ monkeypatch.chdir(folder)
91
+
92
+ main(["init"])
93
+
94
+ for path in _files_under(folder):
95
+ assert b"\r\n" not in (folder / path).read_bytes(), path
96
+ assert (folder / "CLAUDE.md").read_bytes().startswith("# señal Atlas".encode("utf-8"))
97
+
98
+
99
+ def test_keep_what_an_existing_claude_settings_already_allows(tmp_path, monkeypatch):
100
+ (tmp_path / ".claude").mkdir()
101
+ (tmp_path / ".claude/settings.local.json").write_text(
102
+ '{"permissions": {"allow": ["Bash(ls:*)"]}}', encoding="utf-8"
103
+ )
104
+ monkeypatch.chdir(tmp_path)
105
+
106
+ assert main(["init"]) == 0
107
+
108
+ claude = json.loads((tmp_path / ".claude/settings.local.json").read_text(encoding="utf-8"))
109
+ assert claude == {"permissions": {"allow": ["Bash(ls:*)"], "additionalDirectories": []}}
110
+
111
+
112
+ def test_turn_on_repository_detection_in_existing_vscode_settings(tmp_path, monkeypatch):
113
+ (tmp_path / ".vscode").mkdir()
114
+ (tmp_path / ".vscode/settings.json").write_text(
115
+ '{"editor.tabSize": 4, "git.autoRepositoryDetection": "openEditors"}', encoding="utf-8"
116
+ )
117
+ monkeypatch.chdir(tmp_path)
118
+
119
+ assert main(["init"]) == 0
120
+
121
+ vscode = json.loads((tmp_path / ".vscode/settings.json").read_text(encoding="utf-8"))
122
+ assert vscode == {
123
+ "editor.tabSize": 4,
124
+ "git.autoRepositoryDetection": True,
125
+ "git.scanRepositories": [],
126
+ }
127
+
128
+
129
+ def test_keep_an_existing_gitignore_and_ask_to_check_it(tmp_path, monkeypatch, capsys):
130
+ (tmp_path / ".gitignore").write_text("node_modules/\n", encoding="utf-8")
131
+ monkeypatch.chdir(tmp_path)
132
+
133
+ assert main(["init"]) == 0
134
+
135
+ assert (tmp_path / ".gitignore").read_text(encoding="utf-8") == "node_modules/\n"
136
+ assert "Check that the existing .gitignore" in capsys.readouterr().out
137
+
138
+
139
+ def test_refuse_unreadable_claude_settings_and_change_nothing(tmp_path, monkeypatch, capsys):
140
+ (tmp_path / ".claude").mkdir()
141
+ (tmp_path / ".claude/settings.local.json").write_text("{ not json", encoding="utf-8")
142
+ monkeypatch.chdir(tmp_path)
143
+
144
+ assert main(["init"]) == 1
145
+
146
+ assert _files_under(tmp_path) == {".claude/settings.local.json"}
147
+ assert "nothing was changed" in capsys.readouterr().out
148
+
149
+
150
+ def test_refuse_a_folder_that_is_already_an_atlas(tmp_path, monkeypatch, capsys):
151
+ monkeypatch.chdir(tmp_path)
152
+ main(["init"])
153
+ (tmp_path / "STATUS.md").write_text("work in progress", encoding="utf-8")
154
+ capsys.readouterr()
155
+
156
+ assert main(["init"]) == 1
157
+
158
+ assert (tmp_path / "STATUS.md").read_text(encoding="utf-8") == "work in progress"
159
+ assert "already an Atlas" in capsys.readouterr().out
160
+
161
+
162
+ def test_suggest_migrate_for_an_atlas_without_settings(tmp_path, monkeypatch, capsys):
163
+ monkeypatch.chdir(tmp_path)
164
+ (tmp_path / "CLAUDE.md").write_text("an Atlas from before _settings/", encoding="utf-8")
165
+
166
+ assert main(["init"]) == 1
167
+
168
+ assert not (tmp_path / "_settings").exists()
169
+ assert "atlas migrate" in capsys.readouterr().out
170
+
171
+
172
+ def test_leave_existing_sources_untouched(tmp_path, monkeypatch):
173
+ source = tmp_path / "elsewhere" / "log-daemon"
174
+ source.mkdir(parents=True)
175
+ atlas = tmp_path / "acme"
176
+ atlas.mkdir()
177
+ (atlas / "log-daemon").symlink_to(source)
178
+ monkeypatch.chdir(atlas)
179
+
180
+ assert main(["init"]) == 0
181
+
182
+ assert (atlas / "log-daemon").is_symlink()
@@ -0,0 +1,6 @@
1
+ from atlas_cli.cli import main
2
+
3
+
4
+ def test_print_the_usage(capsys):
5
+ assert main([]) == 0
6
+ assert "usage: atlas" in capsys.readouterr().out
@@ -0,0 +1,79 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "iniconfig"
16
+ version = "2.3.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "packaging"
25
+ version = "26.3"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "paranoid-atlas-cli"
34
+ version = "0.1.1"
35
+ source = { editable = "." }
36
+
37
+ [package.dev-dependencies]
38
+ dev = [
39
+ { name = "pytest" },
40
+ ]
41
+
42
+ [package.metadata]
43
+
44
+ [package.metadata.requires-dev]
45
+ dev = [{ name = "pytest", specifier = ">=8" }]
46
+
47
+ [[package]]
48
+ name = "pluggy"
49
+ version = "1.6.0"
50
+ source = { registry = "https://pypi.org/simple" }
51
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
52
+ wheels = [
53
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
54
+ ]
55
+
56
+ [[package]]
57
+ name = "pygments"
58
+ version = "2.21.0"
59
+ source = { registry = "https://pypi.org/simple" }
60
+ sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
61
+ wheels = [
62
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
63
+ ]
64
+
65
+ [[package]]
66
+ name = "pytest"
67
+ version = "9.1.1"
68
+ source = { registry = "https://pypi.org/simple" }
69
+ dependencies = [
70
+ { name = "colorama", marker = "sys_platform == 'win32'" },
71
+ { name = "iniconfig" },
72
+ { name = "packaging" },
73
+ { name = "pluggy" },
74
+ { name = "pygments" },
75
+ ]
76
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
79
+ ]