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.
- labtab-0.1.0/.gitignore +8 -0
- labtab-0.1.0/LICENSE +21 -0
- labtab-0.1.0/PKG-INFO +152 -0
- labtab-0.1.0/README.md +125 -0
- labtab-0.1.0/RELEASING.md +144 -0
- labtab-0.1.0/pyproject.toml +52 -0
- labtab-0.1.0/setup.cfg +4 -0
- labtab-0.1.0/src/labtab/__init__.py +5 -0
- labtab-0.1.0/src/labtab/auth.py +178 -0
- labtab-0.1.0/src/labtab/cli.py +77 -0
- labtab-0.1.0/src/labtab/client.py +316 -0
- labtab-0.1.0/src/labtab/commands/__init__.py +5 -0
- labtab-0.1.0/src/labtab/commands/_common.py +55 -0
- labtab-0.1.0/src/labtab/commands/auth.py +205 -0
- labtab-0.1.0/src/labtab/commands/export.py +238 -0
- labtab-0.1.0/src/labtab/commands/grants.py +77 -0
- labtab-0.1.0/src/labtab/commands/labs.py +39 -0
- labtab-0.1.0/src/labtab/commands/meetings.py +198 -0
- labtab-0.1.0/src/labtab/commands/papers.py +338 -0
- labtab-0.1.0/src/labtab/commands/people.py +70 -0
- labtab-0.1.0/src/labtab/commands/projects.py +70 -0
- labtab-0.1.0/src/labtab/commands/tasks.py +234 -0
- labtab-0.1.0/src/labtab/config.py +141 -0
- labtab-0.1.0/src/labtab/errors.py +60 -0
- labtab-0.1.0/src/labtab/models/__init__.py +48 -0
- labtab-0.1.0/src/labtab/models/api.py +153 -0
- labtab-0.1.0/src/labtab/models/generated.py +1390 -0
- labtab-0.1.0/src/labtab/output.py +149 -0
- labtab-0.1.0/src/labtab/py.typed +0 -0
- labtab-0.1.0/src/labtab.egg-info/PKG-INFO +152 -0
- labtab-0.1.0/src/labtab.egg-info/SOURCES.txt +52 -0
- labtab-0.1.0/src/labtab.egg-info/dependency_links.txt +1 -0
- labtab-0.1.0/src/labtab.egg-info/entry_points.txt +2 -0
- labtab-0.1.0/src/labtab.egg-info/requires.txt +15 -0
- labtab-0.1.0/src/labtab.egg-info/top_level.txt +1 -0
- labtab-0.1.0/tests/__init__.py +0 -0
- labtab-0.1.0/tests/conftest.py +79 -0
- labtab-0.1.0/tests/test_auth.py +206 -0
- labtab-0.1.0/tests/test_client.py +130 -0
- labtab-0.1.0/tests/test_commands_auth.py +152 -0
- labtab-0.1.0/tests/test_commands_export.py +211 -0
- labtab-0.1.0/tests/test_commands_grants.py +107 -0
- labtab-0.1.0/tests/test_commands_labs.py +52 -0
- labtab-0.1.0/tests/test_commands_meetings.py +77 -0
- labtab-0.1.0/tests/test_commands_meetings_create.py +230 -0
- labtab-0.1.0/tests/test_commands_papers.py +82 -0
- labtab-0.1.0/tests/test_commands_papers_import.py +299 -0
- labtab-0.1.0/tests/test_commands_people.py +82 -0
- labtab-0.1.0/tests/test_commands_projects.py +69 -0
- labtab-0.1.0/tests/test_commands_tasks.py +81 -0
- labtab-0.1.0/tests/test_commands_tasks_complete.py +67 -0
- labtab-0.1.0/tests/test_commands_tasks_create.py +176 -0
- labtab-0.1.0/tests/test_config.py +91 -0
- labtab-0.1.0/tests/test_models_generated.py +38 -0
labtab-0.1.0/.gitignore
ADDED
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,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
|
+
}
|