labtab 0.1.0__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.
Files changed (54) hide show
  1. labtab-0.1.0/.gitignore +8 -0
  2. labtab-0.1.0/LICENSE +21 -0
  3. labtab-0.1.0/PKG-INFO +152 -0
  4. labtab-0.1.0/README.md +125 -0
  5. labtab-0.1.0/RELEASING.md +144 -0
  6. labtab-0.1.0/pyproject.toml +52 -0
  7. labtab-0.1.0/setup.cfg +4 -0
  8. labtab-0.1.0/src/labtab/__init__.py +5 -0
  9. labtab-0.1.0/src/labtab/auth.py +178 -0
  10. labtab-0.1.0/src/labtab/cli.py +77 -0
  11. labtab-0.1.0/src/labtab/client.py +316 -0
  12. labtab-0.1.0/src/labtab/commands/__init__.py +5 -0
  13. labtab-0.1.0/src/labtab/commands/_common.py +55 -0
  14. labtab-0.1.0/src/labtab/commands/auth.py +205 -0
  15. labtab-0.1.0/src/labtab/commands/export.py +238 -0
  16. labtab-0.1.0/src/labtab/commands/grants.py +77 -0
  17. labtab-0.1.0/src/labtab/commands/labs.py +39 -0
  18. labtab-0.1.0/src/labtab/commands/meetings.py +198 -0
  19. labtab-0.1.0/src/labtab/commands/papers.py +338 -0
  20. labtab-0.1.0/src/labtab/commands/people.py +70 -0
  21. labtab-0.1.0/src/labtab/commands/projects.py +70 -0
  22. labtab-0.1.0/src/labtab/commands/tasks.py +234 -0
  23. labtab-0.1.0/src/labtab/config.py +141 -0
  24. labtab-0.1.0/src/labtab/errors.py +60 -0
  25. labtab-0.1.0/src/labtab/models/__init__.py +48 -0
  26. labtab-0.1.0/src/labtab/models/api.py +153 -0
  27. labtab-0.1.0/src/labtab/models/generated.py +1390 -0
  28. labtab-0.1.0/src/labtab/output.py +149 -0
  29. labtab-0.1.0/src/labtab/py.typed +0 -0
  30. labtab-0.1.0/src/labtab.egg-info/PKG-INFO +152 -0
  31. labtab-0.1.0/src/labtab.egg-info/SOURCES.txt +52 -0
  32. labtab-0.1.0/src/labtab.egg-info/dependency_links.txt +1 -0
  33. labtab-0.1.0/src/labtab.egg-info/entry_points.txt +2 -0
  34. labtab-0.1.0/src/labtab.egg-info/requires.txt +15 -0
  35. labtab-0.1.0/src/labtab.egg-info/top_level.txt +1 -0
  36. labtab-0.1.0/tests/__init__.py +0 -0
  37. labtab-0.1.0/tests/conftest.py +79 -0
  38. labtab-0.1.0/tests/test_auth.py +206 -0
  39. labtab-0.1.0/tests/test_client.py +130 -0
  40. labtab-0.1.0/tests/test_commands_auth.py +152 -0
  41. labtab-0.1.0/tests/test_commands_export.py +211 -0
  42. labtab-0.1.0/tests/test_commands_grants.py +107 -0
  43. labtab-0.1.0/tests/test_commands_labs.py +52 -0
  44. labtab-0.1.0/tests/test_commands_meetings.py +77 -0
  45. labtab-0.1.0/tests/test_commands_meetings_create.py +230 -0
  46. labtab-0.1.0/tests/test_commands_papers.py +82 -0
  47. labtab-0.1.0/tests/test_commands_papers_import.py +299 -0
  48. labtab-0.1.0/tests/test_commands_people.py +82 -0
  49. labtab-0.1.0/tests/test_commands_projects.py +69 -0
  50. labtab-0.1.0/tests/test_commands_tasks.py +81 -0
  51. labtab-0.1.0/tests/test_commands_tasks_complete.py +67 -0
  52. labtab-0.1.0/tests/test_commands_tasks_create.py +176 -0
  53. labtab-0.1.0/tests/test_config.py +91 -0
  54. labtab-0.1.0/tests/test_models_generated.py +38 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .ruff_cache/
labtab-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wetware Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
labtab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: labtab
3
+ Version: 0.1.0
4
+ Summary: Command-line interface for LabTab (Wetware Ltd). Manage your lab from the terminal.
5
+ Author-email: Wetware Ltd <support@wetware.ai>
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: typer[all]>=0.12.0
15
+ Requires-Dist: httpx>=0.27.0
16
+ Requires-Dist: pydantic>=2.5
17
+ Requires-Dist: pydantic[email]>=2.5
18
+ Requires-Dist: rich>=13.7.0
19
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
20
+ Requires-Dist: tomli-w>=1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.4; extra == "dev"
23
+ Requires-Dist: pytest-httpx>=0.30; extra == "dev"
24
+ Requires-Dist: responses>=0.24; extra == "dev"
25
+ Requires-Dist: ruff>=0.1; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # labtab
29
+
30
+ Command-line interface for [LabTab](https://labtab.app) — the lab management
31
+ platform for Principal Investigators. Manage grants, papers, meetings, tasks,
32
+ projects, and people from your terminal.
33
+
34
+ Built by [Wetware Ltd](https://wetware.co.uk).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install labtab
40
+ ```
41
+
42
+ Requires Python 3.10+.
43
+
44
+ ## Quickstart
45
+
46
+ ```bash
47
+ # Pair your terminal with your LabTab account (opens a browser window)
48
+ labtab auth login
49
+
50
+ # See which labs you belong to
51
+ labtab labs list
52
+
53
+ # List your grants
54
+ labtab grants list
55
+
56
+ # Inspect a single grant
57
+ labtab grants show gr_abc123
58
+
59
+ # Show the next 7 days of meetings
60
+ labtab meetings upcoming --days 7
61
+
62
+ # Get JSON for scripting
63
+ labtab papers list --json | jq '.[] | .title'
64
+
65
+ # Export your whole lab as a zip
66
+ labtab export --format zip --lab chen-neuro-lab
67
+ ```
68
+
69
+ ## Commands
70
+
71
+ All commands support `--json` for machine-readable output and `--lab <slug>` to
72
+ override the default lab from your config.
73
+
74
+ | Command | What it does |
75
+ | ------------------------------ | ----------------------------------------- |
76
+ | `labtab auth login` | Pair this terminal with your account |
77
+ | `labtab auth logout` | Forget the local API key |
78
+ | `labtab auth whoami` | Show the signed-in user and default lab |
79
+ | `labtab auth set-base-url` | Update the stored API base URL |
80
+ | `labtab labs list` | Labs you have membership in |
81
+ | `labtab grants list` | Grants, filter by `--status` |
82
+ | `labtab grants show <id>` | Grant detail card |
83
+ | `labtab papers list` | Papers, filter by `--year` |
84
+ | `labtab papers show <id>` | Paper detail with authors, DOI |
85
+ | `labtab meetings upcoming` | Meetings in the next `--days` (default 7) |
86
+ | `labtab meetings show <id>` | Meeting detail card |
87
+ | `labtab tasks list` | Tasks, filter by `--status` |
88
+ | `labtab tasks show <id>` | Task detail card |
89
+ | `labtab projects list` | Projects |
90
+ | `labtab projects show <id>` | Project detail card |
91
+ | `labtab people list` | Lab members |
92
+ | `labtab people show <id>` | Person detail card |
93
+ | `labtab export` | Download a full lab export |
94
+
95
+ ## Configuration
96
+
97
+ `labtab` reads and writes its config at `~/.config/labtab/config.toml`.
98
+ Override the path with the `LABTAB_CONFIG_FILE` environment variable.
99
+
100
+ ```toml
101
+ [api]
102
+ base_url = "https://labtab.app/api/v1"
103
+
104
+ [auth]
105
+ api_key = "lab_api_xxxxxxxx"
106
+ user_email = "pi@example.com"
107
+ default_lab_id = 42
108
+ default_lab_slug = "chen-neuro-lab"
109
+ ```
110
+
111
+ The config file is written with `0600` permissions so other users on the
112
+ machine cannot read your API key.
113
+
114
+ If you later move the product domain, you can rewrite just the hostname while
115
+ preserving the rest of the API path:
116
+
117
+ ```bash
118
+ labtab auth set-base-url --host labtab.io
119
+ ```
120
+
121
+ ## Pydantic models
122
+
123
+ The package ships two sets of models:
124
+
125
+ * `labtab.models` (re-exports `labtab.models.api`) — hand-curated, **lenient**
126
+ models the CLI uses internally. They tolerate extra or renamed backend
127
+ fields so a CLI install doesn't break when the server ships new columns.
128
+ * `labtab.models.generated` — a **strict**, auto-generated snapshot of the
129
+ backend's OpenAPI schema. Useful if you want to script against the API
130
+ directly with precise types.
131
+
132
+ ### Regenerating the strict snapshot
133
+
134
+ ```bash
135
+ # with a running backend at localhost:8000
136
+ datamodel-codegen \
137
+ --url http://localhost:8000/api/v1/schema/ \
138
+ --output src/labtab/models/generated.py \
139
+ --output-model-type pydantic_v2.BaseModel \
140
+ --input-file-type openapi
141
+
142
+ # or from a snapshotted schema
143
+ cd backend && python manage.py spectacular --file /tmp/schema.yml
144
+ datamodel-codegen --input /tmp/schema.yml \
145
+ --output src/labtab/models/generated.py \
146
+ --output-model-type pydantic_v2.BaseModel \
147
+ --input-file-type openapi
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT. See `LICENSE`.
labtab-0.1.0/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # labtab
2
+
3
+ Command-line interface for [LabTab](https://labtab.app) — the lab management
4
+ platform for Principal Investigators. Manage grants, papers, meetings, tasks,
5
+ projects, and people from your terminal.
6
+
7
+ Built by [Wetware Ltd](https://wetware.co.uk).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install labtab
13
+ ```
14
+
15
+ Requires Python 3.10+.
16
+
17
+ ## Quickstart
18
+
19
+ ```bash
20
+ # Pair your terminal with your LabTab account (opens a browser window)
21
+ labtab auth login
22
+
23
+ # See which labs you belong to
24
+ labtab labs list
25
+
26
+ # List your grants
27
+ labtab grants list
28
+
29
+ # Inspect a single grant
30
+ labtab grants show gr_abc123
31
+
32
+ # Show the next 7 days of meetings
33
+ labtab meetings upcoming --days 7
34
+
35
+ # Get JSON for scripting
36
+ labtab papers list --json | jq '.[] | .title'
37
+
38
+ # Export your whole lab as a zip
39
+ labtab export --format zip --lab chen-neuro-lab
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ All commands support `--json` for machine-readable output and `--lab <slug>` to
45
+ override the default lab from your config.
46
+
47
+ | Command | What it does |
48
+ | ------------------------------ | ----------------------------------------- |
49
+ | `labtab auth login` | Pair this terminal with your account |
50
+ | `labtab auth logout` | Forget the local API key |
51
+ | `labtab auth whoami` | Show the signed-in user and default lab |
52
+ | `labtab auth set-base-url` | Update the stored API base URL |
53
+ | `labtab labs list` | Labs you have membership in |
54
+ | `labtab grants list` | Grants, filter by `--status` |
55
+ | `labtab grants show <id>` | Grant detail card |
56
+ | `labtab papers list` | Papers, filter by `--year` |
57
+ | `labtab papers show <id>` | Paper detail with authors, DOI |
58
+ | `labtab meetings upcoming` | Meetings in the next `--days` (default 7) |
59
+ | `labtab meetings show <id>` | Meeting detail card |
60
+ | `labtab tasks list` | Tasks, filter by `--status` |
61
+ | `labtab tasks show <id>` | Task detail card |
62
+ | `labtab projects list` | Projects |
63
+ | `labtab projects show <id>` | Project detail card |
64
+ | `labtab people list` | Lab members |
65
+ | `labtab people show <id>` | Person detail card |
66
+ | `labtab export` | Download a full lab export |
67
+
68
+ ## Configuration
69
+
70
+ `labtab` reads and writes its config at `~/.config/labtab/config.toml`.
71
+ Override the path with the `LABTAB_CONFIG_FILE` environment variable.
72
+
73
+ ```toml
74
+ [api]
75
+ base_url = "https://labtab.app/api/v1"
76
+
77
+ [auth]
78
+ api_key = "lab_api_xxxxxxxx"
79
+ user_email = "pi@example.com"
80
+ default_lab_id = 42
81
+ default_lab_slug = "chen-neuro-lab"
82
+ ```
83
+
84
+ The config file is written with `0600` permissions so other users on the
85
+ machine cannot read your API key.
86
+
87
+ If you later move the product domain, you can rewrite just the hostname while
88
+ preserving the rest of the API path:
89
+
90
+ ```bash
91
+ labtab auth set-base-url --host labtab.io
92
+ ```
93
+
94
+ ## Pydantic models
95
+
96
+ The package ships two sets of models:
97
+
98
+ * `labtab.models` (re-exports `labtab.models.api`) — hand-curated, **lenient**
99
+ models the CLI uses internally. They tolerate extra or renamed backend
100
+ fields so a CLI install doesn't break when the server ships new columns.
101
+ * `labtab.models.generated` — a **strict**, auto-generated snapshot of the
102
+ backend's OpenAPI schema. Useful if you want to script against the API
103
+ directly with precise types.
104
+
105
+ ### Regenerating the strict snapshot
106
+
107
+ ```bash
108
+ # with a running backend at localhost:8000
109
+ datamodel-codegen \
110
+ --url http://localhost:8000/api/v1/schema/ \
111
+ --output src/labtab/models/generated.py \
112
+ --output-model-type pydantic_v2.BaseModel \
113
+ --input-file-type openapi
114
+
115
+ # or from a snapshotted schema
116
+ cd backend && python manage.py spectacular --file /tmp/schema.yml
117
+ datamodel-codegen --input /tmp/schema.yml \
118
+ --output src/labtab/models/generated.py \
119
+ --output-model-type pydantic_v2.BaseModel \
120
+ --input-file-type openapi
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT. See `LICENSE`.
@@ -0,0 +1,144 @@
1
+ # Releasing `labtab`
2
+
3
+ This document describes how to cut a release of the LabTab CLI Python
4
+ package and ship it to PyPI (or Test PyPI for pre-releases).
5
+
6
+ ## Prerequisites
7
+
8
+ ### Trusted Publishing configuration
9
+
10
+ Release automation lives in
11
+ [`.github/workflows/cli-release.yml`](../../.github/workflows/cli-release.yml)
12
+ and uses PyPI Trusted Publishing via GitHub Actions OIDC. That means:
13
+
14
+ - no long-lived `PYPI_API_TOKEN` / `TEST_PYPI_API_TOKEN` secrets are needed
15
+ - the **first** upload can create the `labtab` project if you configure a
16
+ **pending publisher** first
17
+
18
+ Configure **two** pending publishers before the first tagged release:
19
+
20
+ 1. On **PyPI** (`https://pypi.org`), go to **Account settings -> Publishing**
21
+ and add a **GitHub Actions** publisher for:
22
+ - owner: `alexharston`
23
+ - repository name: `labtab`
24
+ - workflow name: `cli-release.yml`
25
+ - environment name: `pypi`
26
+ - project name: `labtab`
27
+ 2. On **TestPyPI** (`https://test.pypi.org`), repeat the same setup but use:
28
+ - environment name: `testpypi`
29
+ - project name: `labtab`
30
+
31
+ Official docs:
32
+
33
+ - https://docs.pypi.org/trusted-publishers/
34
+ - https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/
35
+
36
+ The workflow also references two GitHub *environments* (`pypi` and
37
+ `testpypi`) so required reviewers / branch protections can be attached
38
+ if desired. Both are optional - an empty environment works.
39
+
40
+ Important: a pending publisher does **not** reserve the package name until the
41
+ first publish actually happens. If `labtab` is still free and you want to claim
42
+ it, create the pending publisher(s) and cut the release promptly.
43
+
44
+ ### Local tooling
45
+
46
+ You only need the release tooling locally if you want to test the
47
+ build before pushing the tag:
48
+
49
+ ```bash
50
+ cd backend/labtab_cli
51
+ python -m pip install --upgrade build twine
52
+ python -m build --sdist --wheel
53
+ twine check dist/*
54
+ ```
55
+
56
+ ## Release steps
57
+
58
+ 1. **Bump the version** in
59
+ [`backend/labtab_cli/pyproject.toml`](./pyproject.toml) (the
60
+ `[project].version` field). Follow PEP 440:
61
+ - `0.1.0` - stable release.
62
+ - `0.1.0rc1`, `0.1.0a2`, `0.1.0b1` - pre-releases (candidate,
63
+ alpha, beta). These ship to **Test PyPI** only.
64
+ 2. **Commit** the version bump:
65
+ ```bash
66
+ git add backend/labtab_cli/pyproject.toml
67
+ git commit -m "chore(cli): bump labtab to 0.1.0"
68
+ ```
69
+ 3. **Tag** the commit with the `labtab/` namespace:
70
+ ```bash
71
+ git tag labtab/v0.1.0
72
+ ```
73
+ For pre-releases: `git tag labtab/v0.1.0rc1` etc.
74
+ 4. **Push** the branch + tag:
75
+ ```bash
76
+ git push origin HEAD
77
+ git push origin labtab/v0.1.0
78
+ ```
79
+ 5. **Watch the workflow** run in the **Actions** tab. The `build`
80
+ job produces and twine-checks the artefacts; the `pypi` or
81
+ `testpypi` job (one of the two, based on the tag) uploads.
82
+
83
+ Tag format reference:
84
+
85
+ | Tag | Destination |
86
+ | --- | --- |
87
+ | `labtab/v0.1.0` | pypi.org |
88
+ | `labtab/v0.1.1` | pypi.org |
89
+ | `labtab/v0.2.0rc1` | test.pypi.org |
90
+ | `labtab/v0.2.0a1` | test.pypi.org |
91
+ | `labtab/v0.2.0b1` | test.pypi.org |
92
+
93
+ ## Verifying a release
94
+
95
+ ### From Test PyPI
96
+
97
+ ```bash
98
+ pip install --index-url https://test.pypi.org/simple/ \
99
+ --extra-index-url https://pypi.org/simple/ \
100
+ labtab==0.2.0rc1
101
+ labtab --help
102
+ ```
103
+
104
+ The `--extra-index-url` fallback is important - Test PyPI does not
105
+ mirror all dependencies, so pip needs the real PyPI as a fallback
106
+ for transitive deps (typer, httpx, ...).
107
+
108
+ ### From PyPI
109
+
110
+ ```bash
111
+ pip install labtab==0.1.0
112
+ labtab --help
113
+ ```
114
+
115
+ ## Rollback
116
+
117
+ PyPI does **not** allow un-publishing a version once it has been
118
+ uploaded. If a broken release ships:
119
+
120
+ 1. **Yank** the release through the PyPI UI:
121
+ https://pypi.org/manage/project/labtab/ -> "Yank" a release.
122
+ Yanked releases remain installable by exact pin but are skipped by
123
+ the resolver by default. This is usually enough to stop new
124
+ installs from picking up the bad version.
125
+ 2. **Ship a new patch release** with the fix (e.g. bump `0.1.0` to
126
+ `0.1.1`) following the release steps above. Never try to re-upload
127
+ the same version number - PyPI will reject duplicate filenames.
128
+ 3. **Test PyPI releases** can be yanked the same way at
129
+ https://test.pypi.org/manage/project/labtab/.
130
+
131
+ ## FAQ
132
+
133
+ - *The workflow says "non-user identities cannot create new projects"* - the
134
+ pending publisher is missing or does not match the repository, workflow,
135
+ environment, or project name exactly.
136
+ - *The workflow says "File already exists"* - this means the version
137
+ number was already uploaded. Bump to a new version and tag again.
138
+ - *A pre-release tag ended up on PyPI* - double-check the tag matches
139
+ `rc`/`a`/`b` at the end. The workflow's branch logic in
140
+ `cli-release.yml` ("Derive version + channel from tag") routes the
141
+ job based on that pattern.
142
+ - *I want a local dry run of the release* - run the build + twine
143
+ check commands from the Local tooling section above. `twine check`
144
+ catches most metadata / README rendering issues before you tag.
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "labtab"
3
+ version = "0.1.0"
4
+ description = "Command-line interface for LabTab (Wetware Ltd). Manage your lab from the terminal."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ authors = [{name = "Wetware Ltd", email = "support@wetware.ai"}]
8
+ license = {text = "MIT"}
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Science/Research",
13
+ "Programming Language :: Python :: 3",
14
+ ]
15
+ dependencies = [
16
+ "typer[all]>=0.12.0",
17
+ "httpx>=0.27.0",
18
+ "pydantic>=2.5",
19
+ "pydantic[email]>=2.5",
20
+ "rich>=13.7.0",
21
+ "tomli>=2.0 ; python_version<'3.11'",
22
+ "tomli-w>=1.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ labtab = "labtab.cli:app"
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest>=7.4", "pytest-httpx>=0.30", "responses>=0.24", "ruff>=0.1"]
30
+
31
+ [build-system]
32
+ requires = ["setuptools>=65", "setuptools-scm>=8"]
33
+ build-backend = "setuptools.build_meta"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ labtab = ["py.typed"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ python_files = ["test_*.py"]
44
+ addopts = "-ra"
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py310"
49
+
50
+ [tool.ruff.lint]
51
+ select = ["E", "F", "I", "W", "B", "UP"]
52
+ ignore = ["E501"]
labtab-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """labtab — command-line interface for LabTab (Wetware Ltd)."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,178 @@
1
+ """Device-pairing primitives for the labtab CLI.
2
+
3
+ The flow implemented by this module is:
4
+
5
+ 1. ``POST /cli/pair/start/`` with ``{client_info: {user_agent, hostname}}``.
6
+ 2. The server returns ``{pairing_code, confirm_url, expires_in}``.
7
+ 3. The user opens ``confirm_url`` in their browser (we try to open it for them
8
+ via :mod:`webbrowser`) and approves the pairing.
9
+ 4. Meanwhile the CLI polls ``GET /cli/pair/poll/?code=<pairing_code>`` every
10
+ ``poll_interval`` seconds. On ``status == "confirmed"`` the server returns
11
+ an API key plus a user profile which we persist to the config file.
12
+ 5. If the server reports ``expired`` or ``denied``, or we exceed ``timeout``
13
+ seconds of polling, we raise :class:`PairingError`.
14
+
15
+ Agent L's backend endpoints are what we consume here. Until those endpoints
16
+ land, tests mock the HTTP responses via :mod:`pytest_httpx`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import platform
22
+ import socket
23
+ import time
24
+ import webbrowser
25
+ from collections.abc import Callable
26
+ from dataclasses import dataclass
27
+ from typing import Any
28
+
29
+ from . import __version__
30
+ from .client import LabtabClient
31
+ from .config import Config
32
+ from .errors import APIError, PairingError
33
+ from .models import CLIPairConfirm, CLIPairStart
34
+
35
+ DEFAULT_POLL_INTERVAL = 2.0
36
+ DEFAULT_TIMEOUT_SECONDS = 600 # 10 minutes
37
+
38
+
39
+ def client_info() -> dict[str, str]:
40
+ """Metadata sent to ``/cli/pair/start/`` so users can identify a terminal."""
41
+
42
+ return {
43
+ "user_agent": f"labtab/{__version__} ({platform.system().lower()})",
44
+ "hostname": socket.gethostname(),
45
+ }
46
+
47
+
48
+ @dataclass
49
+ class PairingResult:
50
+ """Result of a successful pairing — used to populate the config."""
51
+
52
+ api_key: str
53
+ user_email: str | None
54
+ default_lab_id: int | None
55
+ default_lab_slug: str | None
56
+
57
+
58
+ def start_pairing(client: LabtabClient) -> CLIPairStart:
59
+ """Initiate the pairing flow. Does not require authentication."""
60
+
61
+ payload = client.request(
62
+ "POST",
63
+ "/cli/pair/start/",
64
+ json={"client_info": client_info()},
65
+ require_auth=False,
66
+ )
67
+ try:
68
+ return CLIPairStart.model_validate(payload)
69
+ except Exception as exc: # pragma: no cover - validation details
70
+ raise PairingError(f"Unexpected response from /cli/pair/start/: {exc}") from exc
71
+
72
+
73
+ def poll_pairing(client: LabtabClient, code: str) -> CLIPairConfirm:
74
+ """Poll once for the pairing status."""
75
+
76
+ payload = client.request(
77
+ "GET",
78
+ "/cli/pair/poll/",
79
+ params={"code": code},
80
+ require_auth=False,
81
+ )
82
+ try:
83
+ return CLIPairConfirm.model_validate(payload)
84
+ except Exception as exc: # pragma: no cover - validation details
85
+ raise PairingError(f"Unexpected response from /cli/pair/poll/: {exc}") from exc
86
+
87
+
88
+ def maybe_open_browser(url: str, *, enabled: bool) -> None:
89
+ """Open ``url`` in the default browser unless ``enabled`` is False."""
90
+
91
+ if not enabled:
92
+ return
93
+ try:
94
+ webbrowser.open(url)
95
+ except webbrowser.Error: # pragma: no cover - platform specific
96
+ pass
97
+
98
+
99
+ def run_pairing(
100
+ client: LabtabClient,
101
+ *,
102
+ open_browser: bool = True,
103
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
104
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
105
+ on_start: Callable[[CLIPairStart], None] | None = None,
106
+ sleep: Callable[[float], None] = time.sleep,
107
+ now: Callable[[], float] = time.monotonic,
108
+ ) -> PairingResult:
109
+ """Drive the full pairing flow.
110
+
111
+ ``on_start`` is called once with the start payload so callers can print the
112
+ confirmation URL and code. ``sleep`` and ``now`` are injectable for tests.
113
+ """
114
+
115
+ try:
116
+ start = start_pairing(client)
117
+ except APIError as exc:
118
+ raise PairingError(f"Could not start pairing: {exc.message}") from exc
119
+
120
+ if on_start is not None:
121
+ on_start(start)
122
+ maybe_open_browser(start.confirm_url, enabled=open_browser)
123
+
124
+ deadline = now() + min(timeout, start.expires_in or timeout)
125
+
126
+ while True:
127
+ try:
128
+ poll = poll_pairing(client, start.pairing_code)
129
+ except APIError as exc:
130
+ raise PairingError(f"Pairing poll failed: {exc.message}") from exc
131
+
132
+ status = (poll.status or "").lower()
133
+ if status == "confirmed":
134
+ if not poll.api_key:
135
+ raise PairingError(
136
+ "Pairing confirmed but server did not return an API key.",
137
+ )
138
+ return PairingResult(
139
+ api_key=poll.api_key,
140
+ user_email=poll.user_email,
141
+ default_lab_id=poll.default_lab_id,
142
+ default_lab_slug=poll.default_lab_slug,
143
+ )
144
+ if status == "expired":
145
+ raise PairingError("Pairing code expired. Run `labtab auth login` again.")
146
+ if status == "denied":
147
+ raise PairingError("Pairing was denied in the browser.")
148
+
149
+ if now() >= deadline:
150
+ raise PairingError(
151
+ "Timed out waiting for browser confirmation. Run `labtab auth login` again.",
152
+ )
153
+
154
+ sleep(poll_interval)
155
+
156
+
157
+ def persist_pairing(config: Config, result: PairingResult) -> Config:
158
+ """Write ``result`` into ``config`` and save to disk."""
159
+
160
+ config.api_key = result.api_key
161
+ config.user_email = result.user_email
162
+ config.default_lab_id = result.default_lab_id
163
+ config.default_lab_slug = result.default_lab_slug
164
+ config.save()
165
+ return config
166
+
167
+
168
+ def summarise_identity(config: Config) -> dict[str, Any]:
169
+ """Return a plain dict describing the current signed-in identity."""
170
+
171
+ return {
172
+ "signed_in": bool(config.api_key),
173
+ "user_email": config.user_email,
174
+ "default_lab_id": config.default_lab_id,
175
+ "default_lab_slug": config.default_lab_slug,
176
+ "base_url": config.base_url,
177
+ "config_path": str(config.path),
178
+ }