crowdtime-cli 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 (31) hide show
  1. crowdtime_cli-0.1.0/.gitignore +58 -0
  2. crowdtime_cli-0.1.0/LICENSE +77 -0
  3. crowdtime_cli-0.1.0/PKG-INFO +140 -0
  4. crowdtime_cli-0.1.0/README.md +106 -0
  5. crowdtime_cli-0.1.0/pyproject.toml +49 -0
  6. crowdtime_cli-0.1.0/src/crowdtime_cli/__init__.py +3 -0
  7. crowdtime_cli-0.1.0/src/crowdtime_cli/auth.py +69 -0
  8. crowdtime_cli-0.1.0/src/crowdtime_cli/client.py +177 -0
  9. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/__init__.py +1 -0
  10. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/ai_cmd.py +211 -0
  11. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/auth_cmd.py +160 -0
  12. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/clients_cmd.py +150 -0
  13. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/config_cmd.py +91 -0
  14. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/favorites_cmd.py +128 -0
  15. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/log_cmd.py +298 -0
  16. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/org_cmd.py +134 -0
  17. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/projects_cmd.py +175 -0
  18. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/report_cmd.py +242 -0
  19. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/skill_cmd.py +266 -0
  20. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/tasks_cmd.py +101 -0
  21. crowdtime_cli-0.1.0/src/crowdtime_cli/commands/timer_cmd.py +207 -0
  22. crowdtime_cli-0.1.0/src/crowdtime_cli/config.py +125 -0
  23. crowdtime_cli-0.1.0/src/crowdtime_cli/formatters.py +395 -0
  24. crowdtime_cli-0.1.0/src/crowdtime_cli/main.py +334 -0
  25. crowdtime_cli-0.1.0/src/crowdtime_cli/models.py +146 -0
  26. crowdtime_cli-0.1.0/src/crowdtime_cli/oauth.py +107 -0
  27. crowdtime_cli-0.1.0/src/crowdtime_cli/resolvers.py +80 -0
  28. crowdtime_cli-0.1.0/src/crowdtime_cli/skills/crowdtime/SKILL.md +193 -0
  29. crowdtime_cli-0.1.0/src/crowdtime_cli/skills/crowdtime/references/commands.md +659 -0
  30. crowdtime_cli-0.1.0/src/crowdtime_cli/skills/crowdtime/references/workflows.md +286 -0
  31. crowdtime_cli-0.1.0/src/crowdtime_cli/utils.py +166 -0
@@ -0,0 +1,58 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ *.egg
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+ ENV/
15
+
16
+ # Environment
17
+ .env
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+ *~
25
+
26
+ # OS
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Django
31
+ *.log
32
+ db.sqlite3
33
+ staticfiles/
34
+ media/
35
+ logs/
36
+
37
+ # Celery
38
+ celerybeat-schedule
39
+ celerybeat.pid
40
+
41
+ # Coverage
42
+ .coverage
43
+ htmlcov/
44
+ .pytest_cache/
45
+
46
+ # Docker
47
+ docker-compose.override.yml
48
+
49
+ # Claude Code
50
+ .claude/
51
+
52
+ # Frontend
53
+ node_modules/
54
+ .next/
55
+ .env.local
56
+ .env*.local
57
+ RECOVERY.md
58
+ coolify_deployment.md
@@ -0,0 +1,77 @@
1
+ CrowdTime CLI - Proprietary Software License
2
+
3
+ Copyright (c) 2024-2026 IT Crowd SRL. All rights reserved.
4
+
5
+ NOTICE: This software and associated documentation files (the "Software") are
6
+ the proprietary property of IT Crowd SRL ("Company"). The Software is protected
7
+ by copyright laws and international treaty provisions.
8
+
9
+ 1. GRANT OF LICENSE
10
+
11
+ Subject to the terms of this license, the Company grants you a limited,
12
+ non-exclusive, non-transferable, revocable license to install and use the
13
+ Software solely for your internal business purposes in connection with
14
+ a valid CrowdTime subscription.
15
+
16
+ 2. RESTRICTIONS
17
+
18
+ You may NOT:
19
+ a) Copy, modify, adapt, translate, or create derivative works of the Software;
20
+ b) Reverse engineer, decompile, disassemble, or otherwise attempt to derive
21
+ the source code of the Software;
22
+ c) Redistribute, sublicense, rent, lease, lend, sell, or otherwise transfer
23
+ the Software or any rights therein to any third party;
24
+ d) Remove, alter, or obscure any proprietary notices, labels, or marks on
25
+ the Software;
26
+ e) Use the Software to build a competing product or service;
27
+ f) Use the Software in any manner that violates applicable laws or
28
+ regulations.
29
+
30
+ 3. OWNERSHIP
31
+
32
+ The Software is licensed, not sold. The Company retains all right, title,
33
+ and interest in and to the Software, including all intellectual property
34
+ rights therein. No rights are granted to you other than those expressly
35
+ set forth in this license.
36
+
37
+ 4. SUBSCRIPTION REQUIREMENT
38
+
39
+ Use of the Software requires an active CrowdTime subscription. The Company
40
+ reserves the right to verify your subscription status and to disable access
41
+ to the Software if your subscription lapses or is terminated.
42
+
43
+ 5. DATA COLLECTION
44
+
45
+ The Software communicates with CrowdTime servers to provide its
46
+ functionality. By using the Software, you acknowledge that data will be
47
+ transmitted to and stored on CrowdTime servers in accordance with the
48
+ CrowdTime Privacy Policy.
49
+
50
+ 6. NO WARRANTY
51
+
52
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE COMPANY DOES NOT
55
+ WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR UNINTERRUPTED.
56
+
57
+ 7. LIMITATION OF LIABILITY
58
+
59
+ IN NO EVENT SHALL THE COMPANY BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
60
+ SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR
61
+ REVENUE, WHETHER INCURRED DIRECTLY OR INDIRECTLY, OR ANY LOSS OF DATA, USE,
62
+ GOODWILL, OR OTHER INTANGIBLE LOSSES, ARISING OUT OF OR IN CONNECTION WITH
63
+ YOUR USE OF THE SOFTWARE.
64
+
65
+ 8. TERMINATION
66
+
67
+ This license is effective until terminated. The Company may terminate this
68
+ license at any time if you fail to comply with any term herein. Upon
69
+ termination, you must cease all use of the Software and destroy all copies
70
+ in your possession.
71
+
72
+ 9. GOVERNING LAW
73
+
74
+ This license shall be governed by and construed in accordance with the laws
75
+ of Argentina, without regard to its conflict of laws principles.
76
+
77
+ For licensing inquiries: dev@itcrowdarg.com
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: crowdtime-cli
3
+ Version: 0.1.0
4
+ Summary: AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest
5
+ Project-URL: Homepage, https://crowdtime.lat
6
+ Project-URL: Documentation, https://crowdtime.lat/docs
7
+ Project-URL: Support, https://crowdtime.lat/support
8
+ Author-email: IT Crowd <dev@itcrowdarg.com>
9
+ License: Proprietary
10
+ License-File: LICENSE
11
+ Keywords: ai,cli,harvest-alternative,productivity,time-tracking
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: Other/Proprietary License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: humanize>=4.0
26
+ Requires-Dist: keyring>=25.0
27
+ Requires-Dist: platformdirs>=4.0
28
+ Requires-Dist: pydantic>=2.0
29
+ Requires-Dist: python-dateutil>=2.9
30
+ Requires-Dist: rich>=13.0
31
+ Requires-Dist: tomlkit>=0.12.0
32
+ Requires-Dist: typer[all]>=0.12.0
33
+ Description-Content-Type: text/markdown
34
+
35
+ # CrowdTime CLI
36
+
37
+ **AI-powered time tracking from your terminal.** A modern, developer-friendly alternative to Harvest.
38
+
39
+ Track time, manage projects, generate reports, and get AI-powered insights — all without leaving your terminal.
40
+
41
+ ## Features
42
+
43
+ - **Fast time tracking** — Start/stop timers or log entries directly with natural duration formats (`2h30m`, `1:45`, `0.5d`)
44
+ - **AI-powered** — Natural language time entry, smart suggestions based on your patterns, and automatic standup/slack summaries
45
+ - **Rich terminal UI** — Beautiful tables, dashboards, and reports powered by Rich
46
+ - **Multi-org support** — Switch between organizations seamlessly
47
+ - **Secure by default** — API tokens stored in your OS keychain, never in plain text
48
+ - **Flexible reporting** — Daily, weekly, monthly reports in table, JSON, CSV, or Markdown formats
49
+ - **Favorites & templates** — Save common entries for one-command reuse
50
+ - **Claude Code integration** — Ships as a Claude Code skill for AI-assisted time tracking
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install crowdtime-cli
56
+ ```
57
+
58
+ Requires Python 3.11+
59
+
60
+ ## Quick Start
61
+
62
+ ```bash
63
+ # Authenticate
64
+ ct config set server.url https://api.crowdtime.lat
65
+ ct auth login
66
+
67
+ # Switch to your organization
68
+ ct org switch my-org
69
+
70
+ # Start tracking
71
+ ct timer start "Building REST API" -p backend -t "Development"
72
+ ct timer stop
73
+
74
+ # Or log time directly
75
+ ct log -p backend -t "Code Review" 2h "Reviewed auth module PR"
76
+
77
+ # Check your day
78
+ ct status
79
+
80
+ # Weekly report
81
+ ct report --week
82
+ ```
83
+
84
+ ## Command Overview
85
+
86
+ | Command | Alias | Description |
87
+ |---------|-------|-------------|
88
+ | `ct status` | `ct s` | Dashboard: running timer, today's entries, weekly total |
89
+ | `ct timer start` | `ct ts` | Start a timer with project and task |
90
+ | `ct timer stop` | `ct tx` | Stop the running timer and save the entry |
91
+ | `ct log <duration> <desc>` | `ct l` | Log a time entry directly |
92
+ | `ct log list` | `ct ll` | List time entries (today, --week, --month) |
93
+ | `ct report` | `ct r` | Generate reports with grouping and filters |
94
+ | `ct projects list` | `ct p` | List projects |
95
+ | `ct ai suggest` | — | Get AI suggestions based on your patterns |
96
+ | `ct ai summarize` | — | AI-generated summaries (--for standup, --for slack) |
97
+
98
+ ## Duration Formats
99
+
100
+ `2h` | `2h30m` | `2:30` | `150m` | `0.25d` (1 day = 8h) | `1.5` (hours)
101
+
102
+ ## Date Formats
103
+
104
+ `today` | `yesterday` | `monday`..`sunday` | `last friday` | `2026-03-10` | `3/10`
105
+
106
+ ## Configuration
107
+
108
+ ```bash
109
+ ct config set server.url https://api.crowdtime.lat # API server
110
+ ct config set defaults.project my-project # Default project
111
+ ct config set defaults.daily_target 7h # Daily hour target
112
+ ct config list # Show all settings
113
+ ```
114
+
115
+ Config is stored at `~/.crowdtime/config.toml`.
116
+
117
+ ## Authentication
118
+
119
+ CrowdTime supports multiple auth methods:
120
+
121
+ ```bash
122
+ ct auth login # Browser-based Google OAuth (default)
123
+ ct auth login --token <token> # API token (for CI/scripts)
124
+ ct auth login --no-browser # Token prompt (headless environments)
125
+ ```
126
+
127
+ API tokens are stored securely in your OS keychain via [keyring](https://pypi.org/project/keyring/).
128
+
129
+ ## Requirements
130
+
131
+ - Python 3.11+
132
+ - A CrowdTime account at [crowdtime.lat](https://crowdtime.lat)
133
+
134
+ ## Support
135
+
136
+ For issues and feature requests, contact [dev@itcrowdarg.com](mailto:dev@itcrowdarg.com).
137
+
138
+ ## License
139
+
140
+ Proprietary. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,106 @@
1
+ # CrowdTime CLI
2
+
3
+ **AI-powered time tracking from your terminal.** A modern, developer-friendly alternative to Harvest.
4
+
5
+ Track time, manage projects, generate reports, and get AI-powered insights — all without leaving your terminal.
6
+
7
+ ## Features
8
+
9
+ - **Fast time tracking** — Start/stop timers or log entries directly with natural duration formats (`2h30m`, `1:45`, `0.5d`)
10
+ - **AI-powered** — Natural language time entry, smart suggestions based on your patterns, and automatic standup/slack summaries
11
+ - **Rich terminal UI** — Beautiful tables, dashboards, and reports powered by Rich
12
+ - **Multi-org support** — Switch between organizations seamlessly
13
+ - **Secure by default** — API tokens stored in your OS keychain, never in plain text
14
+ - **Flexible reporting** — Daily, weekly, monthly reports in table, JSON, CSV, or Markdown formats
15
+ - **Favorites & templates** — Save common entries for one-command reuse
16
+ - **Claude Code integration** — Ships as a Claude Code skill for AI-assisted time tracking
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install crowdtime-cli
22
+ ```
23
+
24
+ Requires Python 3.11+
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Authenticate
30
+ ct config set server.url https://api.crowdtime.lat
31
+ ct auth login
32
+
33
+ # Switch to your organization
34
+ ct org switch my-org
35
+
36
+ # Start tracking
37
+ ct timer start "Building REST API" -p backend -t "Development"
38
+ ct timer stop
39
+
40
+ # Or log time directly
41
+ ct log -p backend -t "Code Review" 2h "Reviewed auth module PR"
42
+
43
+ # Check your day
44
+ ct status
45
+
46
+ # Weekly report
47
+ ct report --week
48
+ ```
49
+
50
+ ## Command Overview
51
+
52
+ | Command | Alias | Description |
53
+ |---------|-------|-------------|
54
+ | `ct status` | `ct s` | Dashboard: running timer, today's entries, weekly total |
55
+ | `ct timer start` | `ct ts` | Start a timer with project and task |
56
+ | `ct timer stop` | `ct tx` | Stop the running timer and save the entry |
57
+ | `ct log <duration> <desc>` | `ct l` | Log a time entry directly |
58
+ | `ct log list` | `ct ll` | List time entries (today, --week, --month) |
59
+ | `ct report` | `ct r` | Generate reports with grouping and filters |
60
+ | `ct projects list` | `ct p` | List projects |
61
+ | `ct ai suggest` | — | Get AI suggestions based on your patterns |
62
+ | `ct ai summarize` | — | AI-generated summaries (--for standup, --for slack) |
63
+
64
+ ## Duration Formats
65
+
66
+ `2h` | `2h30m` | `2:30` | `150m` | `0.25d` (1 day = 8h) | `1.5` (hours)
67
+
68
+ ## Date Formats
69
+
70
+ `today` | `yesterday` | `monday`..`sunday` | `last friday` | `2026-03-10` | `3/10`
71
+
72
+ ## Configuration
73
+
74
+ ```bash
75
+ ct config set server.url https://api.crowdtime.lat # API server
76
+ ct config set defaults.project my-project # Default project
77
+ ct config set defaults.daily_target 7h # Daily hour target
78
+ ct config list # Show all settings
79
+ ```
80
+
81
+ Config is stored at `~/.crowdtime/config.toml`.
82
+
83
+ ## Authentication
84
+
85
+ CrowdTime supports multiple auth methods:
86
+
87
+ ```bash
88
+ ct auth login # Browser-based Google OAuth (default)
89
+ ct auth login --token <token> # API token (for CI/scripts)
90
+ ct auth login --no-browser # Token prompt (headless environments)
91
+ ```
92
+
93
+ API tokens are stored securely in your OS keychain via [keyring](https://pypi.org/project/keyring/).
94
+
95
+ ## Requirements
96
+
97
+ - Python 3.11+
98
+ - A CrowdTime account at [crowdtime.lat](https://crowdtime.lat)
99
+
100
+ ## Support
101
+
102
+ For issues and feature requests, contact [dev@itcrowdarg.com](mailto:dev@itcrowdarg.com).
103
+
104
+ ## License
105
+
106
+ Proprietary. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "crowdtime-cli"
3
+ version = "0.1.0"
4
+ description = "AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest"
5
+ readme = "README.md"
6
+ license = {text = "Proprietary"}
7
+ authors = [{name = "IT Crowd", email = "dev@itcrowdarg.com"}]
8
+ requires-python = ">=3.11"
9
+ keywords = ["time-tracking", "cli", "productivity", "harvest-alternative", "ai"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: Other/Proprietary License",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Office/Business",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "typer[all]>=0.12.0",
25
+ "rich>=13.0",
26
+ "httpx>=0.27.0",
27
+ "pydantic>=2.0",
28
+ "tomlkit>=0.12.0",
29
+ "keyring>=25.0",
30
+ "platformdirs>=4.0",
31
+ "python-dateutil>=2.9",
32
+ "humanize>=4.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://crowdtime.lat"
37
+ Documentation = "https://crowdtime.lat/docs"
38
+ Support = "https://crowdtime.lat/support"
39
+
40
+ [project.scripts]
41
+ crowdtime = "crowdtime_cli.main:_original_main"
42
+ ct = "crowdtime_cli.main:_original_main"
43
+
44
+ [build-system]
45
+ requires = ["hatchling"]
46
+ build-backend = "hatchling.build"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/crowdtime_cli"]
@@ -0,0 +1,3 @@
1
+ """CrowdTime CLI - AI-powered time tracking from the command line."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,69 @@
1
+ """Authentication and token management for CrowdTime CLI.
2
+
3
+ Stores API tokens in the system keyring, with file-based fallback.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from .config import CONFIG_DIR
11
+
12
+ SERVICE_NAME = "crowdtime"
13
+ USERNAME = "api_token"
14
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials"
15
+
16
+
17
+ def save_token(token: str) -> None:
18
+ """Store the API token, preferring keyring with file fallback."""
19
+ try:
20
+ import keyring
21
+
22
+ keyring.set_password(SERVICE_NAME, USERNAME, token)
23
+ except Exception:
24
+ _save_token_file(token)
25
+
26
+
27
+ def get_token() -> str | None:
28
+ """Retrieve the stored API token."""
29
+ try:
30
+ import keyring
31
+
32
+ token = keyring.get_password(SERVICE_NAME, USERNAME)
33
+ if token:
34
+ return token
35
+ except Exception:
36
+ pass
37
+ return _get_token_file()
38
+
39
+
40
+ def clear_token() -> None:
41
+ """Remove the stored API token."""
42
+ try:
43
+ import keyring
44
+
45
+ keyring.delete_password(SERVICE_NAME, USERNAME)
46
+ except Exception:
47
+ pass
48
+ _clear_token_file()
49
+
50
+
51
+ def _save_token_file(token: str) -> None:
52
+ """File-based fallback for token storage."""
53
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
54
+ CREDENTIALS_FILE.write_text(token)
55
+ CREDENTIALS_FILE.chmod(0o600)
56
+
57
+
58
+ def _get_token_file() -> str | None:
59
+ """File-based fallback for token retrieval."""
60
+ if CREDENTIALS_FILE.exists():
61
+ content = CREDENTIALS_FILE.read_text().strip()
62
+ return content if content else None
63
+ return None
64
+
65
+
66
+ def _clear_token_file() -> None:
67
+ """Remove file-based token."""
68
+ if CREDENTIALS_FILE.exists():
69
+ CREDENTIALS_FILE.unlink()
@@ -0,0 +1,177 @@
1
+ """HTTP client for the CrowdTime API.
2
+
3
+ Wraps httpx with auth header injection, org-scoped URL prefixing,
4
+ and friendly error handling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import httpx
12
+ from rich.console import Console
13
+
14
+ from .auth import get_token
15
+ from .config import get_config
16
+
17
+ console = Console(stderr=True)
18
+
19
+
20
+ class APIError(Exception):
21
+ """Raised when an API call fails."""
22
+
23
+ def __init__(self, message: str, status_code: int | None = None, detail: Any = None) -> None:
24
+ self.message = message
25
+ self.status_code = status_code
26
+ self.detail = detail
27
+ super().__init__(message)
28
+
29
+
30
+ class CrowdTimeClient:
31
+ """Synchronous HTTP client for the CrowdTime API."""
32
+
33
+ def __init__(self, require_auth: bool = True, require_org: bool = False) -> None:
34
+ self.config = get_config()
35
+ self.base_url = self.config.server_url
36
+ self.token = get_token()
37
+ self.org_slug = self.config.organization
38
+
39
+ if require_auth and not self.token:
40
+ console.print(
41
+ "[red]Not authenticated.[/red] Please run [bold]ct login[/bold] first."
42
+ )
43
+ raise SystemExit(1)
44
+
45
+ if require_org and not self.org_slug:
46
+ console.print(
47
+ "[red]No organization set.[/red] Please run [bold]ct org switch <slug>[/bold] first."
48
+ )
49
+ raise SystemExit(1)
50
+
51
+ headers: dict[str, str] = {
52
+ "Content-Type": "application/json",
53
+ "Accept": "application/json",
54
+ }
55
+ if self.token:
56
+ headers["Authorization"] = f"Bearer {self.token}"
57
+
58
+ self._client = httpx.Client(
59
+ base_url=self.base_url,
60
+ headers=headers,
61
+ timeout=30.0,
62
+ )
63
+
64
+ def _org_prefix(self) -> str:
65
+ """Return the org-scoped URL prefix."""
66
+ return f"/api/v1/organizations/{self.org_slug}"
67
+
68
+ def _url(self, path: str, org_scoped: bool = True) -> str:
69
+ """Build the full URL path."""
70
+ if path.startswith("/api/"):
71
+ return path
72
+ if org_scoped and self.org_slug:
73
+ return f"{self._org_prefix()}/{path.lstrip('/')}"
74
+ return f"/api/v1/{path.lstrip('/')}"
75
+
76
+ def get(self, path: str, params: dict[str, Any] | None = None,
77
+ org_scoped: bool = True) -> Any:
78
+ """Make a GET request."""
79
+ url = self._url(path, org_scoped=org_scoped)
80
+ response = self._request("GET", url, params=params)
81
+ return response
82
+
83
+ def post(self, path: str, data: dict[str, Any] | None = None,
84
+ org_scoped: bool = True) -> Any:
85
+ """Make a POST request."""
86
+ url = self._url(path, org_scoped=org_scoped)
87
+ response = self._request("POST", url, json=data)
88
+ return response
89
+
90
+ def patch(self, path: str, data: dict[str, Any] | None = None,
91
+ org_scoped: bool = True) -> Any:
92
+ """Make a PATCH request."""
93
+ url = self._url(path, org_scoped=org_scoped)
94
+ response = self._request("PATCH", url, json=data)
95
+ return response
96
+
97
+ def delete(self, path: str, org_scoped: bool = True) -> Any:
98
+ """Make a DELETE request."""
99
+ url = self._url(path, org_scoped=org_scoped)
100
+ response = self._request("DELETE", url)
101
+ return response
102
+
103
+ def _request(self, method: str, url: str, **kwargs: Any) -> Any:
104
+ """Execute an HTTP request with error handling."""
105
+ try:
106
+ response = self._client.request(method, url, **kwargs)
107
+ return self._handle_response(response)
108
+ except httpx.ConnectError:
109
+ console.print(
110
+ f"[red]Cannot connect to server at {self.base_url}[/red]\n"
111
+ "Is the CrowdTime server running?"
112
+ )
113
+ raise SystemExit(1)
114
+ except httpx.TimeoutException:
115
+ console.print("[red]Request timed out.[/red] Please try again.")
116
+ raise SystemExit(1)
117
+ except APIError:
118
+ raise
119
+ except httpx.HTTPError as e:
120
+ console.print(f"[red]HTTP error:[/red] {e}")
121
+ raise SystemExit(1)
122
+
123
+ def _handle_response(self, response: httpx.Response) -> Any:
124
+ """Check response status and parse JSON."""
125
+ if response.status_code == 204:
126
+ return None
127
+
128
+ if response.status_code == 401:
129
+ console.print(
130
+ "[red]Authentication failed.[/red] Please run [bold]ct login[/bold] to re-authenticate."
131
+ )
132
+ raise SystemExit(1)
133
+
134
+ if response.status_code == 403:
135
+ console.print("[red]Permission denied.[/red] You don't have access to this resource.")
136
+ raise SystemExit(1)
137
+
138
+ if response.status_code == 404:
139
+ raise APIError("Not found", status_code=404)
140
+
141
+ if response.status_code >= 500:
142
+ # Server errors — show a friendly message, not raw HTML
143
+ try:
144
+ detail = response.json()
145
+ msg = detail.get("detail", "Internal server error")
146
+ except Exception:
147
+ msg = "Internal server error"
148
+ raise APIError(
149
+ f"Server error ({response.status_code}): {msg}",
150
+ status_code=response.status_code,
151
+ )
152
+
153
+ if response.status_code >= 400:
154
+ try:
155
+ detail = response.json()
156
+ except Exception:
157
+ detail = response.text
158
+ msg = detail.get("detail", str(detail)) if isinstance(detail, dict) else str(detail)
159
+ raise APIError(msg, status_code=response.status_code, detail=detail)
160
+
161
+ if not response.content:
162
+ return None
163
+
164
+ try:
165
+ return response.json()
166
+ except Exception:
167
+ return response.text
168
+
169
+ def close(self) -> None:
170
+ """Close the underlying HTTP client."""
171
+ self._client.close()
172
+
173
+ def __enter__(self) -> CrowdTimeClient:
174
+ return self
175
+
176
+ def __exit__(self, *args: Any) -> None:
177
+ self.close()
@@ -0,0 +1 @@
1
+ """CrowdTime CLI commands."""