codemie-sdk-python 0.1.433__tar.gz → 0.1.435__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 (62) hide show
  1. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/PKG-INFO +1 -1
  2. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/pyproject.toml +1 -1
  3. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/__init__.py +10 -0
  4. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/auth/credentials.py +63 -0
  5. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/client/client.py +43 -23
  6. codemie_sdk_python-0.1.435/src/codemie_sdk/models/admin.py +87 -0
  7. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/project.py +21 -0
  8. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/skill.py +88 -0
  9. codemie_sdk_python-0.1.435/src/codemie_sdk/services/admin.py +138 -0
  10. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/project.py +4 -0
  11. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/skill.py +86 -0
  12. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/utils/http.py +1 -1
  13. codemie_sdk_python-0.1.433/src/codemie_sdk/models/admin.py +0 -27
  14. codemie_sdk_python-0.1.433/src/codemie_sdk/services/admin.py +0 -58
  15. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/README.md +0 -0
  16. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/auth/__init__.py +0 -0
  17. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/client/__init__.py +0 -0
  18. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/exceptions.py +0 -0
  19. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/__init__.py +0 -0
  20. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/analytics.py +0 -0
  21. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/assistant.py +0 -0
  22. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/categories.py +0 -0
  23. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/common.py +0 -0
  24. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/conversation.py +0 -0
  25. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/datasource.py +0 -0
  26. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/errors.py +0 -0
  27. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/file_operation.py +0 -0
  28. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/guardrails.py +0 -0
  29. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/integration.py +0 -0
  30. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/llm.py +0 -0
  31. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/mermaid.py +0 -0
  32. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/task.py +0 -0
  33. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/user.py +0 -0
  34. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/vendor_assistant.py +0 -0
  35. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/vendor_guardrail.py +0 -0
  36. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/vendor_knowledgebase.py +0 -0
  37. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/vendor_workflow.py +0 -0
  38. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/workflow.py +0 -0
  39. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/workflow_execution_payload.py +0 -0
  40. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/workflow_state.py +0 -0
  41. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/models/workflow_thoughts.py +0 -0
  42. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/analytics.py +0 -0
  43. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/assistant.py +0 -0
  44. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/categories.py +0 -0
  45. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/codemie_guardrails.py +0 -0
  46. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/conversation.py +0 -0
  47. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/datasource.py +0 -0
  48. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/files.py +0 -0
  49. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/integration.py +0 -0
  50. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/llm.py +0 -0
  51. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/mermaid.py +0 -0
  52. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/task.py +0 -0
  53. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/user.py +0 -0
  54. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/vendor_assistant.py +0 -0
  55. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/vendor_guardrail.py +0 -0
  56. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/vendor_knowledgebase.py +0 -0
  57. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/vendor_workflow.py +0 -0
  58. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/webhook.py +0 -0
  59. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/workflow.py +0 -0
  60. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/workflow_execution.py +0 -0
  61. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/services/workflow_execution_state.py +0 -0
  62. {codemie_sdk_python-0.1.433 → codemie_sdk_python-0.1.435}/src/codemie_sdk/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codemie-sdk-python
3
- Version: 0.1.433
3
+ Version: 0.1.435
4
4
  Summary: CodeMie SDK for Python
5
5
  Author: Vadym Vlasenko
6
6
  Author-email: vadym_vlasenko@epam.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codemie-sdk-python"
3
- version = "0.1.433"
3
+ version = "0.1.435"
4
4
  description = "CodeMie SDK for Python"
5
5
  authors = [
6
6
  "Vadym Vlasenko <vadym_vlasenko@epam.com>",
@@ -125,6 +125,11 @@ from .models.skill import (
125
125
  SkillDetailResponse,
126
126
  SkillListResponse,
127
127
  SkillListPaginatedResponse,
128
+ SkillEventRequest,
129
+ SkillEventItem,
130
+ SkillStatsItem,
131
+ SingleSkillStatsResponse,
132
+ SkillPagination,
128
133
  )
129
134
  from .services.skill import SkillService
130
135
 
@@ -221,5 +226,10 @@ __all__ = [
221
226
  "SkillDetailResponse",
222
227
  "SkillListResponse",
223
228
  "SkillListPaginatedResponse",
229
+ "SkillEventRequest",
230
+ "SkillEventItem",
231
+ "SkillStatsItem",
232
+ "SingleSkillStatsResponse",
233
+ "SkillPagination",
224
234
  "SkillService",
225
235
  ]
@@ -1,10 +1,73 @@
1
1
  """Authentication credentials module for CodeMie SDK."""
2
2
 
3
+ import base64
4
+ import json as _json
3
5
  import time
4
6
  import requests
5
7
  from typing import Callable
6
8
 
7
9
 
10
+ def _decode_jwt_exp(token: str) -> float | None:
11
+ """Return the exp claim from a JWT payload without signature verification."""
12
+ try:
13
+ parts = token.split(".")
14
+ if len(parts) != 3:
15
+ return None
16
+ payload = parts[1]
17
+ padding = 4 - len(payload) % 4
18
+ if padding != 4:
19
+ payload += "=" * padding
20
+ claims = _json.loads(base64.urlsafe_b64decode(payload))
21
+ exp = claims.get("exp")
22
+ return float(exp) if exp is not None else None
23
+ except Exception:
24
+ return None
25
+
26
+
27
+ class LocalAuthCredentials:
28
+ """Authenticates against the backend local-auth endpoint and caches the JWT."""
29
+
30
+ _TOKEN_EXPIRY_BUFFER_SECONDS = 30
31
+
32
+ def __init__(
33
+ self,
34
+ api_domain: str,
35
+ email: str,
36
+ password: str,
37
+ verify_ssl: bool = True,
38
+ ):
39
+ self._login_url = f"{api_domain.rstrip('/')}/v1/local-auth/login"
40
+ self._email = email
41
+ self._password = password
42
+ self._verify_ssl = verify_ssl
43
+ self._cached_token: str | None = None
44
+ self._token_expires_at: float = 0.0
45
+
46
+ def get_token(self) -> str:
47
+ """Return cached JWT, re-logging in when expired."""
48
+ now = time.monotonic()
49
+ if self._cached_token and now < self._token_expires_at:
50
+ return self._cached_token
51
+
52
+ response = requests.post(
53
+ self._login_url,
54
+ json={"email": self._email, "password": self._password},
55
+ verify=self._verify_ssl,
56
+ )
57
+ response.raise_for_status()
58
+ self._cached_token = response.json()["access_token"]
59
+
60
+ exp = _decode_jwt_exp(self._cached_token)
61
+ if exp is not None:
62
+ ttl = exp - time.time()
63
+ self._token_expires_at = now + max(
64
+ ttl - self._TOKEN_EXPIRY_BUFFER_SECONDS, 0
65
+ )
66
+ else:
67
+ self._token_expires_at = now + 3570 # 1 h fallback
68
+ return self._cached_token
69
+
70
+
8
71
  class KeycloakCredentials:
9
72
  """Keycloak authentication credentials handler."""
10
73
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Callable
4
4
 
5
- from ..auth.credentials import KeycloakCredentials
5
+ from ..auth.credentials import KeycloakCredentials, LocalAuthCredentials
6
6
  from ..services.admin import AdminService
7
7
  from ..services.analytics import AnalyticsService
8
8
  from ..services.assistant import AssistantService
@@ -56,18 +56,6 @@ class CodeMieClient:
56
56
  external_idp: Identity provider ID to validate extern_token with (optional, required if using external_token)
57
57
  verify_ssl: Whether to verify SSL certificates (default: True)
58
58
  """
59
- self.auth = KeycloakCredentials(
60
- server_url=auth_server_url,
61
- realm_name=auth_realm_name,
62
- client_id=auth_client_id,
63
- client_secret=auth_client_secret,
64
- username=username,
65
- password=password,
66
- external_token=external_token,
67
- external_idp=external_idp,
68
- verify_ssl=verify_ssl,
69
- )
70
-
71
59
  self._token: str | None = None
72
60
  self._api_domain = codemie_api_domain.rstrip("/")
73
61
  self._is_localhost = self._is_localhost_domain(self._api_domain)
@@ -78,10 +66,34 @@ class CodeMieClient:
78
66
 
79
67
  requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
80
68
 
81
- # Initialize token; keep string copy for backward-compat callers reading _token directly
82
- self._token = "" if self._is_localhost else self.auth.get_token()
83
- # Services receive the bound method so each request resolves a fresh (cached) token
84
- _token_source = "" if self._is_localhost else self.auth.get_token
69
+ if self._is_localhost and username and password:
70
+ self._local_auth: LocalAuthCredentials | None = LocalAuthCredentials(
71
+ api_domain=codemie_api_domain,
72
+ email=username,
73
+ password=password,
74
+ verify_ssl=verify_ssl,
75
+ )
76
+ self._token = self._local_auth.get_token()
77
+ _token_source = self._local_auth.get_token
78
+ elif self._is_localhost:
79
+ self._local_auth = None
80
+ self._token = ""
81
+ _token_source = ""
82
+ else:
83
+ self._local_auth = None
84
+ self.auth = KeycloakCredentials(
85
+ server_url=auth_server_url,
86
+ realm_name=auth_realm_name,
87
+ client_id=auth_client_id,
88
+ client_secret=auth_client_secret,
89
+ username=username,
90
+ password=password,
91
+ external_token=external_token,
92
+ external_idp=external_idp,
93
+ verify_ssl=verify_ssl,
94
+ )
95
+ self._token = self.auth.get_token()
96
+ _token_source = self.auth.get_token
85
97
 
86
98
  # Initialize services with verify_ssl parameter and token
87
99
  self.admin = AdminService(
@@ -145,7 +157,10 @@ class CodeMieClient:
145
157
  @property
146
158
  def token(self) -> str:
147
159
  """Get current token or fetch new one if not available."""
148
- self._token = "" if self._is_localhost else self.auth.get_token()
160
+ if self._is_localhost and self._local_auth:
161
+ self._token = self._local_auth.get_token()
162
+ elif not self._is_localhost:
163
+ self._token = self.auth.get_token()
149
164
  return self._token
150
165
 
151
166
  @staticmethod
@@ -162,9 +177,14 @@ class CodeMieClient:
162
177
 
163
178
  def refresh_token(self) -> str:
164
179
  """Force token refresh."""
165
- # Invalidate cache so next get_token() call re-fetches from Keycloak
166
- self.auth._cached_token = None
167
- self.auth._token_expires_at = 0.0
168
- self._token = "" if self._is_localhost else self.auth.get_token()
169
- # Services already hold self.auth.get_token callable, so no reconstruction needed
180
+ if self._is_localhost and self._local_auth:
181
+ self._local_auth._cached_token = None
182
+ self._local_auth._token_expires_at = 0.0
183
+ self._token = self._local_auth.get_token()
184
+ elif self._is_localhost:
185
+ self._token = ""
186
+ else:
187
+ self.auth._cached_token = None
188
+ self.auth._token_expires_at = 0.0
189
+ self._token = self.auth.get_token()
170
190
  return self._token
@@ -0,0 +1,87 @@
1
+ """Admin models for managing applications/projects."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class ApplicationsListResponse(BaseModel):
7
+ """Response model for list applications endpoint."""
8
+
9
+ model_config = ConfigDict(extra="ignore")
10
+
11
+ applications: list[str] = Field(..., description="List of application names")
12
+
13
+
14
+ class ApplicationCreateRequest(BaseModel):
15
+ """Request model for creating an application/project."""
16
+
17
+ model_config = ConfigDict(extra="ignore")
18
+
19
+ name: str = Field(..., description="Application/project name")
20
+
21
+
22
+ class ApplicationCreateResponse(BaseModel):
23
+ """Response model for create application endpoint."""
24
+
25
+ model_config = ConfigDict(extra="ignore")
26
+
27
+ message: str = Field(..., description="Created application name")
28
+
29
+
30
+ class LocalUserCreateRequest(BaseModel):
31
+ """Request model for admin user creation (local auth mode only)."""
32
+
33
+ model_config = ConfigDict(extra="ignore")
34
+
35
+ email: str
36
+ username: str
37
+ password: str
38
+ name: str | None = None
39
+ is_admin: bool = False
40
+ is_maintainer: bool = False
41
+
42
+
43
+ class LocalUserDetail(BaseModel):
44
+ """Response model for created/fetched user (local auth mode)."""
45
+
46
+ model_config = ConfigDict(extra="ignore")
47
+
48
+ id: str
49
+ email: str
50
+ username: str
51
+
52
+
53
+ class AdminUserListItem(BaseModel):
54
+ """User item returned by the admin list-users endpoint."""
55
+
56
+ model_config = ConfigDict(extra="ignore")
57
+
58
+ id: str
59
+ email: str
60
+ username: str
61
+
62
+
63
+ class AdminUserListPagination(BaseModel):
64
+ """Pagination metadata from GET /v1/admin/users."""
65
+
66
+ model_config = ConfigDict(extra="ignore")
67
+
68
+ total: int
69
+ page: int
70
+ per_page: int
71
+
72
+
73
+ class AdminUserListResponse(BaseModel):
74
+ """Paginated response from GET /v1/admin/users."""
75
+
76
+ model_config = ConfigDict(extra="ignore")
77
+
78
+ data: list[AdminUserListItem]
79
+ pagination: AdminUserListPagination
80
+
81
+ @property
82
+ def users(self) -> list[AdminUserListItem]:
83
+ return self.data
84
+
85
+ @property
86
+ def total(self) -> int:
87
+ return self.pagination.total
@@ -43,6 +43,24 @@ class ProjectMember(BaseModel):
43
43
  date: datetime | None = None
44
44
 
45
45
 
46
+ class ProjectBudgetItem(BaseModel):
47
+ """Budget assigned to a project (present when include_budgets=true)."""
48
+
49
+ model_config = ConfigDict(extra="ignore")
50
+
51
+ budget_id: str
52
+ name: str
53
+ budget_category: str
54
+ soft_budget: float
55
+ max_budget: float
56
+ budget_duration: str
57
+ budget_reset_at: datetime | None = None
58
+ provider_sync_status: str
59
+ member_count: int
60
+ allocated_member_budget_total: float
61
+ current_spending: float | None = None
62
+
63
+
46
64
  class ProjectListItem(BaseModel):
47
65
  """Project list response item."""
48
66
 
@@ -58,6 +76,8 @@ class ProjectListItem(BaseModel):
58
76
  counters: ProjectCounters | None = None
59
77
  cost_center_id: UUID | None = None
60
78
  cost_center_name: str | None = None
79
+ spending: float | None = None
80
+ budgets: list[ProjectBudgetItem] = []
61
81
 
62
82
 
63
83
  class PaginationInfo(BaseModel):
@@ -133,6 +153,7 @@ class ProjectUpdateRequest(BaseModel):
133
153
  description: str | None = None
134
154
  cost_center_id: UUID | None = None
135
155
  clear_cost_center: bool = False
156
+ project_member_budget_tracking_enabled: bool | None = None
136
157
 
137
158
 
138
159
  class ProjectDeleteResponse(BaseModel):
@@ -2,6 +2,7 @@
2
2
 
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
+ from typing import Any, Literal
5
6
 
6
7
  from pydantic import BaseModel, ConfigDict, Field
7
8
 
@@ -122,3 +123,90 @@ class SkillReactionResponse(BaseModel):
122
123
  reaction: str
123
124
  reaction_at: str = Field(alias="reactionAt")
124
125
  resource_type: str | None = Field(None, alias="resourceType")
126
+
127
+
128
+ class SkillPagination(BaseModel):
129
+ """Shared pagination envelope for skills statistics endpoints."""
130
+
131
+ model_config = ConfigDict(extra="ignore")
132
+
133
+ page: int
134
+ per_page: int
135
+ total: int
136
+ pages: int
137
+
138
+
139
+ class SkillEventRequest(BaseModel):
140
+ """Request body for POST /v1/skills/events."""
141
+
142
+ session_id: str
143
+ command: Literal["add", "update", "remove", "list", "find"]
144
+ status: Literal["started", "completed", "failed"]
145
+ scope: Literal["global", "project", "unknown"] | None = None
146
+ error_code: str | None = None
147
+ agent_selection_mode: (
148
+ Literal["explicit", "auto_detected", "prompted", "upstream"] | None
149
+ ) = None
150
+ target_agents: list[str] | None = None
151
+ source: str | None = None
152
+ skill_name: str | None = None
153
+ skill_slug: str | None = None
154
+ skill_id: str | None = None
155
+ agent: str = "codemie-skills"
156
+ agent_version: str | None = None
157
+ repository: str | None = None
158
+ branch: str | None = None
159
+ project: str | None = None
160
+ attributes: dict[str, Any] | None = None
161
+
162
+
163
+ class SkillEventItem(BaseModel):
164
+ """Single row from the raw event log."""
165
+
166
+ model_config = ConfigDict(extra="ignore")
167
+
168
+ skill_slug: str | None = None
169
+ source: str | None = None
170
+ target_agents: list[str] = Field(default_factory=list)
171
+ date: datetime
172
+ command: str
173
+ user_id: str
174
+
175
+
176
+ class SkillEventsPaginatedResponse(BaseModel):
177
+ """Paginated response from GET /v1/skills/events."""
178
+
179
+ model_config = ConfigDict(extra="ignore")
180
+
181
+ data: list[SkillEventItem]
182
+ pagination: SkillPagination
183
+
184
+
185
+ class SkillStatsItem(BaseModel):
186
+ """Per-skill aggregated stats item."""
187
+
188
+ model_config = ConfigDict(extra="ignore")
189
+
190
+ skill_slug: str | None = None
191
+ installs: int
192
+ removals: int
193
+ by_agent: dict[str, int] = Field(default_factory=dict)
194
+
195
+
196
+ class SkillStatsPaginatedResponse(BaseModel):
197
+ """Paginated response from GET /v1/skills/events/stats."""
198
+
199
+ model_config = ConfigDict(extra="ignore")
200
+
201
+ data: list[SkillStatsItem]
202
+ pagination: SkillPagination
203
+
204
+
205
+ class SingleSkillStatsResponse(BaseModel):
206
+ """Response from GET /v1/skills/events/{slug}/stats."""
207
+
208
+ model_config = ConfigDict(extra="ignore")
209
+
210
+ installs: int
211
+ removals: int
212
+ by_agent: dict[str, int] = Field(default_factory=dict)
@@ -0,0 +1,138 @@
1
+ """Admin service implementation."""
2
+
3
+ from ..models.admin import (
4
+ AdminUserListItem,
5
+ AdminUserListResponse,
6
+ ApplicationsListResponse,
7
+ ApplicationCreateRequest,
8
+ ApplicationCreateResponse,
9
+ LocalUserCreateRequest,
10
+ LocalUserDetail,
11
+ )
12
+ from ..utils import ApiRequestHandler, TokenSource
13
+
14
+
15
+ class AdminService:
16
+ """Service for managing CodeMie applications/projects."""
17
+
18
+ def __init__(self, api_domain: str, token: TokenSource, verify_ssl: bool = True):
19
+ """Initialize the admin service.
20
+
21
+ Args:
22
+ api_domain: Base URL for the CodeMie API
23
+ token: Authentication token
24
+ verify_ssl: Whether to verify SSL certificates
25
+ """
26
+ self._api = ApiRequestHandler(api_domain, token, verify_ssl)
27
+
28
+ def list_applications(self, project_name: str | None = None) -> list[str]:
29
+ """Get list of all applications/projects.
30
+
31
+ Args:
32
+ project_name: Optional project name to filter by
33
+
34
+ Returns:
35
+ List of application names
36
+ """
37
+ params = {}
38
+ if project_name:
39
+ params["search"] = project_name
40
+
41
+ response = self._api.get(
42
+ "/v1/admin/applications",
43
+ ApplicationsListResponse,
44
+ params=params if params else None,
45
+ )
46
+ return response.applications
47
+
48
+ def create_application(self, request: ApplicationCreateRequest) -> str:
49
+ """Create a new application/project.
50
+
51
+ Args:
52
+ request: Application creation request
53
+
54
+ Returns:
55
+ Created application name
56
+ """
57
+ response = self._api.post(
58
+ "/v1/admin/application",
59
+ ApplicationCreateResponse,
60
+ json_data=request.model_dump(exclude_none=True),
61
+ )
62
+ return response.message
63
+
64
+ def create_local_user(
65
+ self,
66
+ email: str,
67
+ username: str,
68
+ password: str,
69
+ name: str | None = None,
70
+ is_admin: bool = False,
71
+ is_maintainer: bool = False,
72
+ ) -> LocalUserDetail:
73
+ """Create a user via the admin API (local auth mode only).
74
+
75
+ Args:
76
+ email: User email address
77
+ username: Username (3-50 chars)
78
+ password: Initial password
79
+ name: Optional display name
80
+ is_admin: Grant admin role
81
+ is_maintainer: Grant maintainer role
82
+
83
+ Returns:
84
+ Created user details
85
+ """
86
+ request = LocalUserCreateRequest(
87
+ email=email,
88
+ username=username,
89
+ password=password,
90
+ name=name,
91
+ is_admin=is_admin,
92
+ is_maintainer=is_maintainer,
93
+ )
94
+ return self._api.post(
95
+ "/v1/admin/users",
96
+ LocalUserDetail,
97
+ json_data=request.model_dump(exclude_none=True),
98
+ wrap_response=False,
99
+ )
100
+
101
+ def list_users(
102
+ self,
103
+ search: str | None = None,
104
+ page: int = 0,
105
+ per_page: int = 20,
106
+ ) -> list[AdminUserListItem]:
107
+ """List users via the admin API.
108
+
109
+ Args:
110
+ search: Search string matched against email, username, name
111
+ page: 0-indexed page number
112
+ per_page: Page size
113
+
114
+ Returns:
115
+ List of user items
116
+ """
117
+ params: dict = {"page": page, "per_page": per_page}
118
+ if search:
119
+ params["search"] = search
120
+ response = self._api.get(
121
+ "/v1/admin/users",
122
+ AdminUserListResponse,
123
+ params=params,
124
+ wrap_response=False,
125
+ )
126
+ return response.users
127
+
128
+ def delete_user(self, user_id: str) -> None:
129
+ """Deactivate (soft-delete) a user via the admin API.
130
+
131
+ Args:
132
+ user_id: ID of the user to deactivate
133
+ """
134
+ self._api.delete(
135
+ f"/v1/admin/users/{user_id}",
136
+ dict,
137
+ wrap_response=False,
138
+ )
@@ -86,6 +86,7 @@ class ProjectService:
86
86
  page: int = 0,
87
87
  per_page: int = 20,
88
88
  include_counters: bool = True,
89
+ include_budgets: bool = False,
89
90
  sort_by: Literal["name", "created_at"] | None = None,
90
91
  sort_order: Literal["asc", "desc"] = "asc",
91
92
  ) -> PaginatedProjectListResponse:
@@ -105,6 +106,7 @@ class ProjectService:
105
106
  "page": page,
106
107
  "per_page": per_page,
107
108
  "include_counters": include_counters,
109
+ "include_budgets": include_budgets,
108
110
  "sort_order": sort_order,
109
111
  }
110
112
 
@@ -147,6 +149,7 @@ class ProjectService:
147
149
  description: str | None = None,
148
150
  cost_center_id: UUID | None = None,
149
151
  clear_cost_center: bool = False,
152
+ project_member_budget_tracking_enabled: bool | None = None,
150
153
  ) -> ProjectUpdateResponse:
151
154
  """Update a project.
152
155
 
@@ -168,6 +171,7 @@ class ProjectService:
168
171
  description=description,
169
172
  cost_center_id=cost_center_id,
170
173
  clear_cost_center=clear_cost_center,
174
+ project_member_budget_tracking_enabled=project_member_budget_tracking_enabled,
171
175
  )
172
176
 
173
177
  return self._api.patch(
@@ -11,10 +11,14 @@ from ..models.skill import (
11
11
  SkillCategory,
12
12
  SkillCreateRequest,
13
13
  SkillDetailResponse,
14
+ SkillEventItem,
15
+ SkillEventRequest,
14
16
  SkillImportRequest,
15
17
  SkillListPaginatedResponse,
16
18
  SkillListResponse,
19
+ SkillStatsItem,
17
20
  SkillUpdateRequest,
21
+ SingleSkillStatsResponse,
18
22
  )
19
23
  from ..utils.http import ApiRequestHandler, TokenSource
20
24
 
@@ -302,3 +306,85 @@ class SkillService:
302
306
  Removal confirmation
303
307
  """
304
308
  return self._api.delete(f"/v1/skills/{skill_id}/reactions", dict)
309
+
310
+ # Statistics / Event Log
311
+
312
+ def record_event(self, request: SkillEventRequest) -> dict:
313
+ """Record a skill lifecycle event.
314
+
315
+ Args:
316
+ request: Skill event request
317
+
318
+ Returns:
319
+ Confirmation dict
320
+ """
321
+ return self._api.post(
322
+ "/v1/skills/events",
323
+ dict,
324
+ json_data=request.model_dump(exclude_none=True),
325
+ )
326
+
327
+ def get_events(
328
+ self,
329
+ from_: str | None = None,
330
+ to: str | None = None,
331
+ limit: int = 100,
332
+ offset: int = 0,
333
+ ) -> list[SkillEventItem]:
334
+ """Get raw skill event log.
335
+
336
+ Args:
337
+ from_: ISO datetime lower bound (inclusive)
338
+ to: ISO datetime upper bound (inclusive)
339
+ limit: Maximum number of results
340
+ offset: Pagination offset
341
+
342
+ Returns:
343
+ List of skill event items
344
+ """
345
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
346
+ if from_ is not None:
347
+ params["from"] = from_
348
+ if to is not None:
349
+ params["to"] = to
350
+ return self._api.get("/v1/skills/events", list[SkillEventItem], params=params)
351
+
352
+ def get_events_stats(
353
+ self,
354
+ from_: str | None = None,
355
+ to: str | None = None,
356
+ limit: int = 100,
357
+ offset: int = 0,
358
+ ) -> list[SkillStatsItem]:
359
+ """Get per-skill aggregated install/removal counts.
360
+
361
+ Args:
362
+ from_: ISO datetime lower bound (inclusive)
363
+ to: ISO datetime upper bound (inclusive)
364
+ limit: Maximum number of results
365
+ offset: Pagination offset
366
+
367
+ Returns:
368
+ List of skill stats items
369
+ """
370
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
371
+ if from_ is not None:
372
+ params["from"] = from_
373
+ if to is not None:
374
+ params["to"] = to
375
+ return self._api.get(
376
+ "/v1/skills/events/stats", list[SkillStatsItem], params=params
377
+ )
378
+
379
+ def get_skill_stats(self, slug: str) -> SingleSkillStatsResponse:
380
+ """Get aggregated stats for a single skill.
381
+
382
+ Args:
383
+ slug: Skill slug identifier
384
+
385
+ Returns:
386
+ Single skill stats
387
+ """
388
+ return self._api.get(
389
+ f"/v1/skills/events/{slug}/stats", SingleSkillStatsResponse
390
+ )
@@ -86,7 +86,7 @@ class ApiRequestHandler:
86
86
  headers["Content-Type"] = "application/json"
87
87
 
88
88
  if include_auth:
89
- if self._is_localhost:
89
+ if self._is_localhost and not self._token:
90
90
  headers["Authorization"] = "dev-codemie-user"
91
91
  else:
92
92
  token_value = self._token() if callable(self._token) else self._token
@@ -1,27 +0,0 @@
1
- """Admin models for managing applications/projects."""
2
-
3
- from pydantic import BaseModel, ConfigDict, Field
4
-
5
-
6
- class ApplicationsListResponse(BaseModel):
7
- """Response model for list applications endpoint."""
8
-
9
- model_config = ConfigDict(extra="ignore")
10
-
11
- applications: list[str] = Field(..., description="List of application names")
12
-
13
-
14
- class ApplicationCreateRequest(BaseModel):
15
- """Request model for creating an application/project."""
16
-
17
- model_config = ConfigDict(extra="ignore")
18
-
19
- name: str = Field(..., description="Application/project name")
20
-
21
-
22
- class ApplicationCreateResponse(BaseModel):
23
- """Response model for create application endpoint."""
24
-
25
- model_config = ConfigDict(extra="ignore")
26
-
27
- message: str = Field(..., description="Created application name")
@@ -1,58 +0,0 @@
1
- """Admin service implementation."""
2
-
3
- from ..models.admin import (
4
- ApplicationsListResponse,
5
- ApplicationCreateRequest,
6
- ApplicationCreateResponse,
7
- )
8
- from ..utils import ApiRequestHandler, TokenSource
9
-
10
-
11
- class AdminService:
12
- """Service for managing CodeMie applications/projects."""
13
-
14
- def __init__(self, api_domain: str, token: TokenSource, verify_ssl: bool = True):
15
- """Initialize the admin service.
16
-
17
- Args:
18
- api_domain: Base URL for the CodeMie API
19
- token: Authentication token
20
- verify_ssl: Whether to verify SSL certificates
21
- """
22
- self._api = ApiRequestHandler(api_domain, token, verify_ssl)
23
-
24
- def list_applications(self, project_name: str | None = None) -> list[str]:
25
- """Get list of all applications/projects.
26
-
27
- Args:
28
- project_name: Optional project name to filter by
29
-
30
- Returns:
31
- List of application names
32
- """
33
- params = {}
34
- if project_name:
35
- params["search"] = project_name
36
-
37
- response = self._api.get(
38
- "/v1/admin/applications",
39
- ApplicationsListResponse,
40
- params=params if params else None,
41
- )
42
- return response.applications
43
-
44
- def create_application(self, request: ApplicationCreateRequest) -> str:
45
- """Create a new application/project.
46
-
47
- Args:
48
- request: Application creation request
49
-
50
- Returns:
51
- Created application name
52
- """
53
- response = self._api.post(
54
- "/v1/admin/application",
55
- ApplicationCreateResponse,
56
- json_data=request.model_dump(exclude_none=True),
57
- )
58
- return response.message