Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions infisical_sdk/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,102 @@ class KmsKeyEncryptDataResponse(BaseModel):
class KmsKeyDecryptDataResponse(BaseModel):
"""Response model for decrypt data API"""
plaintext: str

@dataclass
class CreateFolderResponseItem(BaseModel):
"""Folder model with path for create response"""
id: str
name: str
createdAt: str
updatedAt: str
envId: str
path: str
version: Optional[int] = 1
parentId: Optional[str] = None
isReserved: Optional[bool] = False
description: Optional[str] = None
lastSecretModified: Optional[str] = None

@dataclass
class CreateFolderResponse(BaseModel):
"""Response model for create folder API"""
folder: CreateFolderResponseItem

@classmethod
def from_dict(cls, data: Dict) -> 'CreateFolderResponse':
return cls(
folder=CreateFolderResponseItem.from_dict(data['folder']),
)


@dataclass
class ListFoldersResponseItem(BaseModel):
"""Response model for list folders API"""
id: str
name: str
createdAt: str
updatedAt: str
envId: str
version: Optional[int] = 1
parentId: Optional[str] = None
isReserved: Optional[bool] = False
description: Optional[str] = None
lastSecretModified: Optional[str] = None
relativePath: Optional[str] = None


@dataclass
class ListFoldersResponse(BaseModel):
"""Complete response model for folders API"""
folders: List[ListFoldersResponseItem]

@classmethod
def from_dict(cls, data: Dict) -> 'ListFoldersResponse':
"""Create model from dictionary with camelCase keys, handling nested objects"""
return cls(
folders=[ListFoldersResponseItem.from_dict(folder) for folder in data['folders']]
)


@dataclass
class Environment(BaseModel):
"""Environment model"""
envId: str
envName: str
envSlug: str

@dataclass
class SingleFolderResponseItem(BaseModel):
"""Response model for get folder API"""
id: str
name: str
createdAt: str
updatedAt: str
envId: str
path: str
projectId: str
environment: Environment
version: Optional[int] = 1
parentId: Optional[str] = None
isReserved: Optional[bool] = False
description: Optional[str] = None
lastSecretModified: Optional[str] = None

@classmethod
def from_dict(cls, data: Dict) -> 'SingleFolderResponseItem':
"""Create model from dictionary with nested Environment"""
folder_data = data.copy()
folder_data['environment'] = Environment.from_dict(data['environment'])

return super().from_dict(folder_data)

@dataclass
class SingleFolderResponse(BaseModel):
"""Response model for get/create folder API"""
folder: SingleFolderResponseItem

@classmethod
def from_dict(cls, data: Dict) -> 'SingleFolderResponse':
return cls(
folder=SingleFolderResponseItem.from_dict(data['folder']),
)
2 changes: 2 additions & 0 deletions infisical_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from infisical_sdk.resources import Auth
from infisical_sdk.resources import V3RawSecrets
from infisical_sdk.resources import KMS
from infisical_sdk.resources import V2Folders

from infisical_sdk.util import SecretsCache

Expand All @@ -24,6 +25,7 @@ def __init__(self, host: str, token: str = None, cache_ttl: int = 60):
self.auth = Auth(self.api, self.set_token)
self.secrets = V3RawSecrets(self.api, self.cache)
self.kms = KMS(self.api)
self.folders = V2Folders(self.api)

def set_token(self, token: str):
"""
Expand Down
3 changes: 2 additions & 1 deletion infisical_sdk/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .secrets import V3RawSecrets
from .kms import KMS
from .auth import Auth
from .auth import Auth
from .folders import V2Folders
75 changes: 75 additions & 0 deletions infisical_sdk/resources/folders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Optional
from datetime import datetime, timezone

from infisical_sdk.infisical_requests import InfisicalRequests
from infisical_sdk.api_types import ListFoldersResponse, SingleFolderResponse, SingleFolderResponseItem, CreateFolderResponse, CreateFolderResponseItem


class V2Folders:
def __init__(self, requests: InfisicalRequests) -> None:
self.requests = requests

def create_folder(
self,
name: str,
environment_slug: str,
project_id: str,
path: str = "/",
description: Optional[str] = None) -> CreateFolderResponseItem:

request_body = {
"projectId": project_id,
"environment": environment_slug,
"name": name,
"path": path,
"description": description,
}

result = self.requests.post(
path="/api/v2/folders",
json=request_body,
model=CreateFolderResponse
)

return result.data.folder

def list_folders(
self,
project_id: str,
environment_slug: str,
path: str,
last_secret_modified: Optional[datetime] = None,
recursive: bool = False) -> ListFoldersResponse:

params = {
"projectId": project_id,
"environment": environment_slug,
"path": path,
"recursive": recursive,
}

if last_secret_modified is not None:
# Convert to UTC and format as RFC 3339 with 'Z' suffix
# The API expects UTC times in 'Z' format (e.g., 2023-11-07T05:31:56Z)
Comment on lines +52 to +53
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utc_datetime = last_secret_modified.astimezone(timezone.utc) if last_secret_modified.tzinfo else last_secret_modified.replace(tzinfo=timezone.utc)
params["lastSecretModified"] = utc_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')

result = self.requests.get(
path="/api/v2/folders",
params=params,
model=ListFoldersResponse
)

return result.data

def get_folder_by_id(
self,
id: str) -> SingleFolderResponseItem:

result = self.requests.get(
path=f"/api/v2/folders/{id}",
model=SingleFolderResponse
)

return result.data.folder