fleet-python 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of fleet-python might be problematic. Click here for more details.
- examples/browser_control_example.py +51 -0
- fleet/__init__.py +4 -16
- fleet/base.py +74 -0
- fleet/client.py +115 -279
- fleet/env/__init__.py +3 -25
- fleet/env/base.py +50 -318
- fleet/env/client.py +241 -0
- fleet/env/models.py +127 -0
- fleet/models.py +109 -0
- fleet/resources/base.py +23 -0
- fleet/resources/browser.py +18 -0
- fleet/resources/sqlite.py +41 -0
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/METADATA +2 -1
- fleet_python-0.2.0.dist-info/RECORD +19 -0
- fleet/config.py +0 -125
- fleet/env/factory.py +0 -446
- fleet/facets/__init__.py +0 -7
- fleet/facets/base.py +0 -223
- fleet/facets/factory.py +0 -29
- fleet/manager_client.py +0 -177
- fleet_python-0.1.0.dist-info/RECORD +0 -17
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/WHEEL +0 -0
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.1.0.dist-info → fleet_python-0.2.0.dist-info}/top_level.txt +0 -0
fleet/env/models.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: openapi_2.json
|
|
3
|
+
# timestamp: 2025-07-08T20:02:11+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BrowserDescribeResponse(BaseModel):
|
|
14
|
+
success: bool = Field(..., title="Success")
|
|
15
|
+
url: str = Field(..., title="Url")
|
|
16
|
+
devtools_url: str = Field(..., title="Devtools Url")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BrowserStartRequest(BaseModel):
|
|
20
|
+
start_page: Optional[str] = Field("about:blank", title="Start Page")
|
|
21
|
+
resolution: Optional[str] = Field("1920x1080", title="Resolution")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BrowserStartResponse(BaseModel):
|
|
25
|
+
success: bool = Field(..., title="Success")
|
|
26
|
+
message: str = Field(..., title="Message")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CreateSnapshotsResponse(BaseModel):
|
|
30
|
+
success: bool = Field(..., title="Success")
|
|
31
|
+
initial_snapshot_path: Optional[str] = Field(None, title="Initial Snapshot Path")
|
|
32
|
+
final_snapshot_path: Optional[str] = Field(None, title="Final Snapshot Path")
|
|
33
|
+
message: str = Field(..., title="Message")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HealthResponse(BaseModel):
|
|
37
|
+
status: str = Field(..., title="Status")
|
|
38
|
+
timestamp: str = Field(..., title="Timestamp")
|
|
39
|
+
service: str = Field(..., title="Service")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LogActionRequest(BaseModel):
|
|
43
|
+
action_type: str = Field(..., title="Action Type")
|
|
44
|
+
sql: Optional[str] = Field(None, title="Sql")
|
|
45
|
+
args: Optional[str] = Field(None, title="Args")
|
|
46
|
+
path: Optional[str] = Field(None, title="Path")
|
|
47
|
+
raw_payload: Optional[str] = Field(None, title="Raw Payload")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LogActionResponse(BaseModel):
|
|
51
|
+
success: bool = Field(..., title="Success")
|
|
52
|
+
message: str = Field(..., title="Message")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class QueryRequest(BaseModel):
|
|
56
|
+
query: str = Field(..., title="Query")
|
|
57
|
+
args: Optional[List] = Field(None, title="Args")
|
|
58
|
+
read_only: Optional[bool] = Field(True, title="Read Only")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class QueryResponse(BaseModel):
|
|
62
|
+
success: bool = Field(..., title="Success")
|
|
63
|
+
columns: Optional[List[str]] = Field(None, title="Columns")
|
|
64
|
+
rows: Optional[List[List]] = Field(None, title="Rows")
|
|
65
|
+
rows_affected: Optional[int] = Field(None, title="Rows Affected")
|
|
66
|
+
last_insert_id: Optional[int] = Field(None, title="Last Insert Id")
|
|
67
|
+
error: Optional[str] = Field(None, title="Error")
|
|
68
|
+
message: str = Field(..., title="Message")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ResetRequest(BaseModel):
|
|
72
|
+
seed: Optional[int] = Field(None, title="Seed")
|
|
73
|
+
timestamp: Optional[str] = Field(None, title="Timestamp")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ResetResponse(BaseModel):
|
|
77
|
+
success: bool = Field(..., title="Success")
|
|
78
|
+
message: str = Field(..., title="Message")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ResourceMode(Enum):
|
|
82
|
+
ro = "ro"
|
|
83
|
+
rw = "rw"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ResourceType(Enum):
|
|
87
|
+
sqlite = "sqlite"
|
|
88
|
+
cdp = "cdp"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TableSchema(BaseModel):
|
|
92
|
+
name: str = Field(..., title="Name")
|
|
93
|
+
sql: str = Field(..., title="Sql")
|
|
94
|
+
columns: List[Dict[str, Any]] = Field(..., title="Columns")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TimestampResponse(BaseModel):
|
|
98
|
+
timestamp: str = Field(..., title="Timestamp")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ValidationError(BaseModel):
|
|
102
|
+
loc: List[Union[str, int]] = Field(..., title="Location")
|
|
103
|
+
msg: str = Field(..., title="Message")
|
|
104
|
+
type: str = Field(..., title="Error Type")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DescribeResponse(BaseModel):
|
|
108
|
+
success: bool = Field(..., title="Success")
|
|
109
|
+
resource_name: str = Field(..., title="Resource Name")
|
|
110
|
+
tables: Optional[List[TableSchema]] = Field(None, title="Tables")
|
|
111
|
+
error: Optional[str] = Field(None, title="Error")
|
|
112
|
+
message: str = Field(..., title="Message")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class HTTPValidationError(BaseModel):
|
|
116
|
+
detail: Optional[List[ValidationError]] = Field(None, title="Detail")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Resource(BaseModel):
|
|
120
|
+
name: str = Field(..., title="Name")
|
|
121
|
+
type: ResourceType
|
|
122
|
+
mode: ResourceMode
|
|
123
|
+
label: Optional[str] = Field(None, title="Label")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ResourcesResponse(BaseModel):
|
|
127
|
+
resources: List[Resource] = Field(..., title="Resources")
|
fleet/models.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: openapi.json
|
|
3
|
+
# timestamp: 2025-07-08T18:21:47+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Environment(BaseModel):
|
|
14
|
+
env_key: str = Field(..., title="Env Key")
|
|
15
|
+
name: str = Field(..., title="Name")
|
|
16
|
+
description: Optional[str] = Field(..., title="Description")
|
|
17
|
+
default_version: Optional[str] = Field(..., title="Default Version")
|
|
18
|
+
versions: Dict[str, str] = Field(..., title="Versions")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Instance(BaseModel):
|
|
22
|
+
instance_id: str = Field(..., title="Instance Id")
|
|
23
|
+
env_key: str = Field(..., title="Env Key")
|
|
24
|
+
version: str = Field(..., title="Version")
|
|
25
|
+
status: str = Field(..., title="Status")
|
|
26
|
+
subdomain: str = Field(..., title="Subdomain")
|
|
27
|
+
created_at: str = Field(..., title="Created At")
|
|
28
|
+
updated_at: str = Field(..., title="Updated At")
|
|
29
|
+
terminated_at: Optional[str] = Field(None, title="Terminated At")
|
|
30
|
+
team_id: str = Field(..., title="Team Id")
|
|
31
|
+
region: str = Field(..., title="Region")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InstanceRequest(BaseModel):
|
|
35
|
+
env_key: str = Field(..., title="Env Key")
|
|
36
|
+
version: Optional[str] = Field(None, title="Version")
|
|
37
|
+
region: Optional[str] = Field("us-east-2", title="Region")
|
|
38
|
+
seed: Optional[int] = Field(None, title="Seed")
|
|
39
|
+
timestamp: Optional[int] = Field(None, title="Timestamp")
|
|
40
|
+
p_error: Optional[float] = Field(None, title="P Error")
|
|
41
|
+
avg_latency: Optional[float] = Field(None, title="Avg Latency")
|
|
42
|
+
run_id: Optional[str] = Field(None, title="Run Id")
|
|
43
|
+
task_id: Optional[str] = Field(None, title="Task Id")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InstanceStatus(Enum):
|
|
47
|
+
pending = "pending"
|
|
48
|
+
running = "running"
|
|
49
|
+
stopped = "stopped"
|
|
50
|
+
error = "error"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ManagerURLs(BaseModel):
|
|
54
|
+
api: str = Field(..., title="Api")
|
|
55
|
+
docs: str = Field(..., title="Docs")
|
|
56
|
+
reset: str = Field(..., title="Reset")
|
|
57
|
+
diff: str = Field(..., title="Diff")
|
|
58
|
+
snapshot: str = Field(..., title="Snapshot")
|
|
59
|
+
execute_verifier_function: str = Field(..., title="Execute Verifier Function")
|
|
60
|
+
execute_verifier_function_with_upload: str = Field(
|
|
61
|
+
..., title="Execute Verifier Function With Upload"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ValidationError(BaseModel):
|
|
66
|
+
loc: List[Union[str, int]] = Field(..., title="Location")
|
|
67
|
+
msg: str = Field(..., title="Message")
|
|
68
|
+
type: str = Field(..., title="Error Type")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class HTTPValidationError(BaseModel):
|
|
72
|
+
detail: Optional[List[ValidationError]] = Field(None, title="Detail")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class InstanceURLs(BaseModel):
|
|
76
|
+
root: str = Field(..., title="Root")
|
|
77
|
+
app: str = Field(..., title="App")
|
|
78
|
+
api: Optional[str] = Field(None, title="Api")
|
|
79
|
+
health: Optional[str] = Field(None, title="Health")
|
|
80
|
+
api_docs: Optional[str] = Field(None, title="Api Docs")
|
|
81
|
+
manager: ManagerURLs
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class InstanceResponse(BaseModel):
|
|
85
|
+
instance_id: str = Field(..., title="Instance Id")
|
|
86
|
+
env_key: str = Field(..., title="Env Key")
|
|
87
|
+
version: str = Field(..., title="Version")
|
|
88
|
+
status: str = Field(..., title="Status")
|
|
89
|
+
subdomain: str = Field(..., title="Subdomain")
|
|
90
|
+
created_at: str = Field(..., title="Created At")
|
|
91
|
+
updated_at: str = Field(..., title="Updated At")
|
|
92
|
+
terminated_at: Optional[str] = Field(None, title="Terminated At")
|
|
93
|
+
team_id: str = Field(..., title="Team Id")
|
|
94
|
+
region: str = Field(..., title="Region")
|
|
95
|
+
urls: InstanceURLs
|
|
96
|
+
health: Optional[bool] = Field(None, title="Health")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class InstanceRecord(BaseModel):
|
|
100
|
+
instance_id: str
|
|
101
|
+
env_key: str
|
|
102
|
+
version: str
|
|
103
|
+
status: str
|
|
104
|
+
subdomain: str
|
|
105
|
+
created_at: str
|
|
106
|
+
updated_at: str
|
|
107
|
+
terminated_at: Optional[str] = None
|
|
108
|
+
team_id: str
|
|
109
|
+
region: str
|
fleet/resources/base.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from ..env.models import Resource as ResourceModel, ResourceType, ResourceMode
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Resource(ABC):
|
|
6
|
+
def __init__(self, resource: ResourceModel):
|
|
7
|
+
self.resource = resource
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def uri(self) -> str:
|
|
11
|
+
return f"{self.resource.type}://{self.resource.name}"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def name(self) -> str:
|
|
15
|
+
return self.resource.name
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def type(self) -> ResourceType:
|
|
19
|
+
return self.resource.type
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def mode(self) -> ResourceMode:
|
|
23
|
+
return self.resource.mode
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ..env.models import Resource as ResourceModel
|
|
2
|
+
from ..env.models import BrowserDescribeResponse
|
|
3
|
+
from .base import Resource
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..env.base import AsyncWrapper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncBrowserResource(Resource):
|
|
12
|
+
def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
|
|
13
|
+
super().__init__(resource)
|
|
14
|
+
self.client = client
|
|
15
|
+
|
|
16
|
+
async def describe(self) -> BrowserDescribeResponse:
|
|
17
|
+
response = await self.client.request("GET", "/resource/cdp/describe")
|
|
18
|
+
return BrowserDescribeResponse(**response.json())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
from ..env.models import Resource as ResourceModel
|
|
3
|
+
from ..env.models import DescribeResponse, QueryRequest, QueryResponse
|
|
4
|
+
from .base import Resource
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..env.base import AsyncWrapper
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncSQLiteResource(Resource):
|
|
13
|
+
def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
|
|
14
|
+
super().__init__(resource)
|
|
15
|
+
self.client = client
|
|
16
|
+
|
|
17
|
+
async def describe(self) -> DescribeResponse:
|
|
18
|
+
"""Describe the SQLite database schema."""
|
|
19
|
+
response = await self.client.request(
|
|
20
|
+
"GET", f"/resource/sqlite/{self.resource.name}/describe"
|
|
21
|
+
)
|
|
22
|
+
return DescribeResponse(**response.json())
|
|
23
|
+
|
|
24
|
+
async def query(
|
|
25
|
+
self, query: str, args: Optional[List[Any]] = None
|
|
26
|
+
) -> QueryResponse:
|
|
27
|
+
return await self._query(query, args, read_only=True)
|
|
28
|
+
|
|
29
|
+
async def exec(self, query: str, args: Optional[List[Any]] = None) -> QueryResponse:
|
|
30
|
+
return await self._query(query, args, read_only=False)
|
|
31
|
+
|
|
32
|
+
async def _query(
|
|
33
|
+
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
34
|
+
) -> QueryResponse:
|
|
35
|
+
request = QueryRequest(query=query, args=args, read_only=read_only)
|
|
36
|
+
response = await self.client.request(
|
|
37
|
+
"POST",
|
|
38
|
+
f"/resource/sqlite/{self.resource.name}/query",
|
|
39
|
+
json=request.model_dump(),
|
|
40
|
+
)
|
|
41
|
+
return QueryResponse(**response.json())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleet-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python SDK for Fleet environments
|
|
5
5
|
Author-email: Fleet AI <nic@fleet.so>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: aiohttp>=3.8.0
|
|
24
24
|
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
26
|
Requires-Dist: typing-extensions>=4.0.0
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
examples/browser_control_example.py,sha256=2nEzUc83bliqR9jYTenkrLTGoKSkWahjP4WpiaHIMxw,1520
|
|
2
|
+
examples/quickstart.py,sha256=AnLlLQYRfrP7y2J69d1HZRlZsjJT-KeBu897MoNfqYM,4671
|
|
3
|
+
fleet/__init__.py,sha256=F8esCp6DcdAc4JAhWlCd_G3pBKRBzuLuVZnElWv5GGQ,987
|
|
4
|
+
fleet/base.py,sha256=lgKhPZhLotP4Iqn7Z4nQTfCvuD3bT37MoucTrSRE2zs,2006
|
|
5
|
+
fleet/client.py,sha256=xMyKc49YwTAh1F5Wbk5hdaB4F6wlrl41a3EjyBdjFUk,5660
|
|
6
|
+
fleet/exceptions.py,sha256=yG3QWprCw1OnF-vdFBFJWE4m3ftBLBng31Dr__VbjI4,2249
|
|
7
|
+
fleet/models.py,sha256=Jf6Zmk689TPXhTSnVENK_VCw0VsujWzEWsN3T29MQ0k,3713
|
|
8
|
+
fleet/env/__init__.py,sha256=ScTHui5BAFTTKKZ6TxS71D5r-JYMxxFEkcT_AcAXDsM,145
|
|
9
|
+
fleet/env/base.py,sha256=bm6BGd81TnTDqE_qG6S50Xhikf9DwNqEEk1uKFY7dEk,1540
|
|
10
|
+
fleet/env/client.py,sha256=yVS4cblGlARWeoZC69Uz_KR2wlmYC19d-BsQalLB9kw,8043
|
|
11
|
+
fleet/env/models.py,sha256=mxRoQEzswMEjwdEN-gn2ylG9Z8IZH-Gqs6hVG0fkSVs,3883
|
|
12
|
+
fleet/resources/base.py,sha256=rzFXBoFqtkM0HZpwqN9NHiwQbmeKOGOYh7G4REaHKGc,553
|
|
13
|
+
fleet/resources/browser.py,sha256=mC1g7xpyvEK8vDGXNn0MXGc8TGE83PIW3KOOj9i3rIA,591
|
|
14
|
+
fleet/resources/sqlite.py,sha256=hZA2qcd7SqjsACJEkKzXQwlJCtvNZ9yom2HCjKy7lOs,1484
|
|
15
|
+
fleet_python-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
+
fleet_python-0.2.0.dist-info/METADATA,sha256=qrpy0oFm5wfUvW7VVqbrdZX566B-CB1iJSp4spH9gmg,3069
|
|
17
|
+
fleet_python-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
fleet_python-0.2.0.dist-info/top_level.txt,sha256=AOyXOrBXUjPcH4BumElz_D95kiWKNIpUbUPFP_9gCLk,15
|
|
19
|
+
fleet_python-0.2.0.dist-info/RECORD,,
|
fleet/config.py
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
"""Fleet SDK Configuration Management."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import logging
|
|
5
|
-
from typing import Optional, Dict, Any
|
|
6
|
-
from pydantic import BaseModel, Field, validator
|
|
7
|
-
from .exceptions import FleetAuthenticationError, FleetConfigurationError
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class FleetConfig(BaseModel):
|
|
14
|
-
"""Fleet SDK Configuration."""
|
|
15
|
-
|
|
16
|
-
api_key: Optional[str] = Field(None, description="Fleet API key")
|
|
17
|
-
base_url: str = Field(default="https://fleet.new", description="Fleet API base URL (hardcoded)")
|
|
18
|
-
|
|
19
|
-
@validator('api_key')
|
|
20
|
-
def validate_api_key(cls, v):
|
|
21
|
-
"""Validate API key format."""
|
|
22
|
-
if v is not None and not _is_valid_api_key(v):
|
|
23
|
-
raise FleetAuthenticationError(
|
|
24
|
-
"Invalid API key format. Fleet API keys should start with 'sk_' followed by alphanumeric characters."
|
|
25
|
-
)
|
|
26
|
-
return v
|
|
27
|
-
|
|
28
|
-
@validator('base_url')
|
|
29
|
-
def validate_base_url(cls, v):
|
|
30
|
-
"""Validate base URL format."""
|
|
31
|
-
if not v.startswith(('http://', 'https://')):
|
|
32
|
-
raise FleetConfigurationError("Base URL must start with 'http://' or 'https://'")
|
|
33
|
-
return v.rstrip('/')
|
|
34
|
-
|
|
35
|
-
def mask_sensitive_data(self) -> Dict[str, Any]:
|
|
36
|
-
"""Return config dict with sensitive data masked."""
|
|
37
|
-
data = self.dict()
|
|
38
|
-
if data.get('api_key'):
|
|
39
|
-
data['api_key'] = _mask_api_key(data['api_key'])
|
|
40
|
-
return data
|
|
41
|
-
|
|
42
|
-
class Config:
|
|
43
|
-
"""Pydantic configuration."""
|
|
44
|
-
extra = 'allow'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_config(**kwargs: Any) -> FleetConfig:
|
|
48
|
-
"""Get Fleet configuration from environment variables.
|
|
49
|
-
|
|
50
|
-
Loads FLEET_API_KEY from environment variables. The base URL is hardcoded to https://fleet.new.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
**kwargs: Override specific configuration values
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
FleetConfig instance
|
|
57
|
-
|
|
58
|
-
Raises:
|
|
59
|
-
FleetAuthenticationError: If API key is invalid
|
|
60
|
-
FleetConfigurationError: If configuration is invalid
|
|
61
|
-
"""
|
|
62
|
-
# Load from environment variables
|
|
63
|
-
config_data = _load_env_config()
|
|
64
|
-
|
|
65
|
-
# Apply any overrides
|
|
66
|
-
config_data.update(kwargs)
|
|
67
|
-
|
|
68
|
-
# Create and validate configuration
|
|
69
|
-
try:
|
|
70
|
-
config = FleetConfig(**config_data)
|
|
71
|
-
return config
|
|
72
|
-
|
|
73
|
-
except Exception as e:
|
|
74
|
-
if isinstance(e, (FleetAuthenticationError, FleetConfigurationError)):
|
|
75
|
-
raise
|
|
76
|
-
raise FleetConfigurationError(f"Invalid configuration: {e}")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _load_env_config() -> Dict[str, Any]:
|
|
80
|
-
"""Load configuration from environment variables."""
|
|
81
|
-
env_mapping = {
|
|
82
|
-
'FLEET_API_KEY': 'api_key',
|
|
83
|
-
# base_url is hardcoded, not configurable via env var
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
config = {}
|
|
87
|
-
for env_var, config_key in env_mapping.items():
|
|
88
|
-
value = os.getenv(env_var)
|
|
89
|
-
if value is not None:
|
|
90
|
-
config[config_key] = value
|
|
91
|
-
|
|
92
|
-
return config
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _is_valid_api_key(api_key: str) -> bool:
|
|
96
|
-
"""Validate API key format."""
|
|
97
|
-
if not api_key:
|
|
98
|
-
return False
|
|
99
|
-
|
|
100
|
-
# Fleet API keys start with 'sk_' followed by alphanumeric characters
|
|
101
|
-
# This is a basic format check - actual validation happens on the server
|
|
102
|
-
if not api_key.startswith('sk_'):
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
# Check if the rest contains only alphanumeric characters and underscores
|
|
106
|
-
key_part = api_key[3:] # Remove 'sk_' prefix
|
|
107
|
-
if not key_part or not key_part.replace('_', '').isalnum():
|
|
108
|
-
return False
|
|
109
|
-
|
|
110
|
-
# Minimum length check
|
|
111
|
-
if len(api_key) < 20:
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
return True
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _mask_api_key(api_key: str) -> str:
|
|
118
|
-
"""Mask API key for logging."""
|
|
119
|
-
if not api_key:
|
|
120
|
-
return api_key
|
|
121
|
-
|
|
122
|
-
if len(api_key) < 8:
|
|
123
|
-
return '*' * len(api_key)
|
|
124
|
-
|
|
125
|
-
return api_key[:4] + '*' * (len(api_key) - 8) + api_key[-4:]
|