swipeflow-api 1.1.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.
@@ -0,0 +1,63 @@
1
+ """SwipeFlow Python SDK for automation workflow approvals.
2
+
3
+ A Pythonic SDK for integrating SwipeFlow approval workflows into your applications.
4
+
5
+ Example:
6
+ >>> from swipeflow_api import SwipeFlowClient
7
+ >>> client = SwipeFlowClient(api_key="your-api-key")
8
+ >>> projects = client.projects.list()
9
+ >>> items = client.items.list(project_id="project-123")
10
+ """
11
+
12
+ __version__ = "1.1.0"
13
+ __author__ = "SwipeFlow"
14
+ __email__ = "support@swipeflow.io"
15
+ __license__ = "ISC"
16
+
17
+ from .client import SwipeFlowClient
18
+ from .models import (
19
+ User,
20
+ Project,
21
+ Item,
22
+ ItemVersion,
23
+ Decision,
24
+ Webhook,
25
+ WebhookEvent,
26
+ ItemStatus,
27
+ DecisionType,
28
+ ProjectRole,
29
+ ContentType,
30
+ ApiKey,
31
+ )
32
+ from .exceptions import (
33
+ SwipeFlowError,
34
+ AuthenticationError,
35
+ AuthorizationError,
36
+ NotFoundError,
37
+ ValidationError,
38
+ ServerError,
39
+ )
40
+
41
+ __all__ = [
42
+ "SwipeFlowClient",
43
+ # Models
44
+ "User",
45
+ "Project",
46
+ "Item",
47
+ "ItemVersion",
48
+ "Decision",
49
+ "Webhook",
50
+ "WebhookEvent",
51
+ "ItemStatus",
52
+ "DecisionType",
53
+ "ProjectRole",
54
+ "ContentType",
55
+ "ApiKey",
56
+ # Exceptions
57
+ "SwipeFlowError",
58
+ "AuthenticationError",
59
+ "AuthorizationError",
60
+ "NotFoundError",
61
+ "ValidationError",
62
+ "ServerError",
63
+ ]
@@ -0,0 +1,161 @@
1
+ """Main SwipeFlow API client."""
2
+
3
+ from typing import Optional
4
+ import requests
5
+ from requests.adapters import HTTPAdapter
6
+ from urllib3.util.retry import Retry
7
+
8
+ from .resources import ProjectsResource, ItemsResource, WebhooksResource, ApiKeysResource, AuthResource
9
+ from .exceptions import raise_for_status
10
+
11
+
12
+ class SwipeFlowClient:
13
+ """Main client for interacting with the SwipeFlow API.
14
+
15
+ Example:
16
+ >>> client = SwipeFlowClient(api_key="your-api-key")
17
+ >>> projects = client.projects.list()
18
+ >>> items = client.items.list(project_id="project-123")
19
+ """
20
+
21
+ DEFAULT_BASE_URL = "https://api.swipeflow.io"
22
+ DEFAULT_TIMEOUT = 30
23
+
24
+ def __init__(
25
+ self,
26
+ api_key: Optional[str] = None,
27
+ base_url: str = DEFAULT_BASE_URL,
28
+ timeout: int = DEFAULT_TIMEOUT,
29
+ verify_ssl: bool = True,
30
+ ):
31
+ """Initialize SwipeFlow API client.
32
+
33
+ Args:
34
+ api_key: API key for authentication. If not provided, will look for
35
+ SWIPEFLOW_API_KEY environment variable.
36
+ base_url: Base URL for the API. Defaults to production API.
37
+ timeout: Request timeout in seconds.
38
+ verify_ssl: Whether to verify SSL certificates.
39
+
40
+ Raises:
41
+ ValueError: If no API key is provided and environment variable not set.
42
+ """
43
+ import os
44
+
45
+ self.api_key = api_key or os.getenv("SWIPEFLOW_API_KEY")
46
+ if not self.api_key:
47
+ raise ValueError(
48
+ "API key not provided. Pass it to SwipeFlowClient(api_key='...') "
49
+ "or set SWIPEFLOW_API_KEY environment variable."
50
+ )
51
+
52
+ self.base_url = base_url.rstrip("/")
53
+ self.timeout = timeout
54
+ self.verify_ssl = verify_ssl
55
+
56
+ self._session = self._create_session()
57
+
58
+ # Initialize resources
59
+ self.projects = ProjectsResource(self)
60
+ self.items = ItemsResource(self)
61
+ self.webhooks = WebhooksResource(self)
62
+ self.api_keys = ApiKeysResource(self)
63
+ self.auth = AuthResource(self)
64
+
65
+ def _create_session(self) -> requests.Session:
66
+ """Create a requests session with retry strategy.
67
+
68
+ Returns:
69
+ Configured requests session
70
+ """
71
+ session = requests.Session()
72
+
73
+ # Set up retry strategy
74
+ retry_strategy = Retry(
75
+ total=3,
76
+ backoff_factor=1,
77
+ status_forcelist=[429, 500, 502, 503, 504],
78
+ allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"],
79
+ )
80
+ adapter = HTTPAdapter(max_retries=retry_strategy)
81
+ session.mount("https://", adapter)
82
+ session.mount("http://", adapter)
83
+
84
+ # Set headers
85
+ session.headers.update(
86
+ {
87
+ "X-API-Key": self.api_key,
88
+ "Content-Type": "application/json",
89
+ "User-Agent": "swipeflow-python-sdk/1.1.0",
90
+ }
91
+ )
92
+
93
+ return session
94
+
95
+ def request(
96
+ self,
97
+ method: str,
98
+ endpoint: str,
99
+ json_data: Optional[dict] = None,
100
+ params: Optional[dict] = None,
101
+ headers: Optional[dict] = None,
102
+ ) -> dict:
103
+ """Make an HTTP request to the API.
104
+
105
+ Args:
106
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
107
+ endpoint: API endpoint (without base URL)
108
+ json_data: JSON body data
109
+ params: Query parameters
110
+ headers: Additional headers
111
+
112
+ Returns:
113
+ Response JSON as dictionary
114
+
115
+ Raises:
116
+ Various SwipeFlowError subclasses depending on response
117
+ """
118
+ url = f"{self.base_url}{endpoint}"
119
+
120
+ req_headers = headers or {}
121
+
122
+ response = self._session.request(
123
+ method=method,
124
+ url=url,
125
+ json=json_data,
126
+ params=params,
127
+ headers=req_headers,
128
+ timeout=self.timeout,
129
+ verify=self.verify_ssl,
130
+ )
131
+
132
+ # Handle errors
133
+ if response.status_code >= 400:
134
+ try:
135
+ error_data = response.json()
136
+ message = error_data.get("error", response.text)
137
+ error_code = error_data.get("code")
138
+ details = error_data.get("details")
139
+ except Exception:
140
+ message = response.text or f"HTTP {response.status_code}"
141
+ error_code = None
142
+ details = None
143
+
144
+ raise_for_status(response.status_code, message, error_code, details)
145
+
146
+ # Handle successful response
147
+ if response.text:
148
+ return response.json()
149
+ return {}
150
+
151
+ def close(self) -> None:
152
+ """Close the client session."""
153
+ self._session.close()
154
+
155
+ def __enter__(self):
156
+ """Context manager entry."""
157
+ return self
158
+
159
+ def __exit__(self, exc_type, exc_val, exc_tb):
160
+ """Context manager exit."""
161
+ self.close()
@@ -0,0 +1,110 @@
1
+ """Exception classes for SwipeFlow API client."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class SwipeFlowError(Exception):
7
+ """Base exception for all SwipeFlow API errors."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: Optional[int] = None,
13
+ error_code: Optional[str] = None,
14
+ details: Optional[Dict[str, Any]] = None,
15
+ ):
16
+ self.message = message
17
+ self.status_code = status_code
18
+ self.error_code = error_code
19
+ self.details = details or {}
20
+ super().__init__(self._format_message())
21
+
22
+ def _format_message(self) -> str:
23
+ """Format the error message."""
24
+ parts = [self.message]
25
+ if self.status_code:
26
+ parts.append(f"(HTTP {self.status_code})")
27
+ if self.error_code:
28
+ parts.append(f"[{self.error_code}]")
29
+ return " ".join(parts)
30
+
31
+
32
+ class AuthenticationError(SwipeFlowError):
33
+ """Raised when authentication fails (401 Unauthorized)."""
34
+
35
+ pass
36
+
37
+
38
+ class AuthorizationError(SwipeFlowError):
39
+ """Raised when user lacks permissions (403 Forbidden)."""
40
+
41
+ pass
42
+
43
+
44
+ class NotFoundError(SwipeFlowError):
45
+ """Raised when a resource is not found (404 Not Found)."""
46
+
47
+ pass
48
+
49
+
50
+ class ValidationError(SwipeFlowError):
51
+ """Raised when request validation fails (400 Bad Request)."""
52
+
53
+ pass
54
+
55
+
56
+ class ConflictError(SwipeFlowError):
57
+ """Raised when a resource conflict occurs (409 Conflict)."""
58
+
59
+ pass
60
+
61
+
62
+ class ServerError(SwipeFlowError):
63
+ """Raised when the server encounters an error (5xx)."""
64
+
65
+ pass
66
+
67
+
68
+ class NetworkError(SwipeFlowError):
69
+ """Raised when a network error occurs."""
70
+
71
+ pass
72
+
73
+
74
+ def raise_for_status(
75
+ status_code: int,
76
+ message: str,
77
+ error_code: Optional[str] = None,
78
+ details: Optional[Dict[str, Any]] = None,
79
+ ) -> None:
80
+ """Raise appropriate exception based on status code.
81
+
82
+ Args:
83
+ status_code: HTTP status code
84
+ message: Error message
85
+ error_code: API-specific error code
86
+ details: Additional error details
87
+
88
+ Raises:
89
+ AuthenticationError: For 401 responses
90
+ AuthorizationError: For 403 responses
91
+ NotFoundError: For 404 responses
92
+ ValidationError: For 400 responses
93
+ ConflictError: For 409 responses
94
+ ServerError: For 5xx responses
95
+ SwipeFlowError: For other errors
96
+ """
97
+ if status_code == 400:
98
+ raise ValidationError(message, status_code, error_code, details)
99
+ elif status_code == 401:
100
+ raise AuthenticationError(message, status_code, error_code, details)
101
+ elif status_code == 403:
102
+ raise AuthorizationError(message, status_code, error_code, details)
103
+ elif status_code == 404:
104
+ raise NotFoundError(message, status_code, error_code, details)
105
+ elif status_code == 409:
106
+ raise ConflictError(message, status_code, error_code, details)
107
+ elif status_code >= 500:
108
+ raise ServerError(message, status_code, error_code, details)
109
+ else:
110
+ raise SwipeFlowError(message, status_code, error_code, details)
@@ -0,0 +1,211 @@
1
+ """Pydantic models for SwipeFlow API resources."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ItemStatus(str, Enum):
11
+ """Status of an item."""
12
+
13
+ PENDING = "PENDING"
14
+ APPROVED = "APPROVED"
15
+ REJECTED = "REJECTED"
16
+ CHANGE_REQUESTED = "CHANGE_REQUESTED"
17
+ PROCESSED = "PROCESSED"
18
+
19
+
20
+ class DecisionType(str, Enum):
21
+ """Type of decision on an item."""
22
+
23
+ APPROVED = "APPROVED"
24
+ REJECTED = "REJECTED"
25
+ CHANGE_REQUESTED = "CHANGE_REQUESTED"
26
+
27
+
28
+ class ProjectRole(str, Enum):
29
+ """Role in a project."""
30
+
31
+ OWNER = "OWNER"
32
+ ADMIN = "ADMIN"
33
+ EDITOR = "EDITOR"
34
+ VIEWER = "VIEWER"
35
+
36
+
37
+ class ContentType(str, Enum):
38
+ """Type of content."""
39
+
40
+ TEXT = "TEXT"
41
+ HTML = "HTML"
42
+ IMAGE = "IMAGE"
43
+ VIDEO = "VIDEO"
44
+ AUDIO = "AUDIO"
45
+
46
+
47
+ class WebhookEvent(str, Enum):
48
+ """Types of webhook events."""
49
+
50
+ ITEM_CREATED = "item.created"
51
+ ITEM_UPDATED = "item.updated"
52
+ ITEM_DELETED = "item.deleted"
53
+ ITEM_APPROVED = "item.approved"
54
+ ITEM_REJECTED = "item.rejected"
55
+ ITEM_CHANGE_REQUESTED = "item.change_requested"
56
+ ITEM_PROCESSED = "item.processed"
57
+ PROJECT_TRIGGER = "project.trigger"
58
+
59
+
60
+ class User(BaseModel):
61
+ """User model."""
62
+
63
+ id: str = Field(..., description="User ID")
64
+ email: str = Field(..., description="Email address")
65
+ name: Optional[str] = Field(None, description="User's full name")
66
+ avatar_url: Optional[str] = Field(None, description="Avatar URL")
67
+ created_at: datetime = Field(..., description="Account creation time")
68
+ updated_at: datetime = Field(..., description="Last update time")
69
+
70
+ class Config:
71
+ use_enum_values = True
72
+
73
+
74
+ class ProjectMember(BaseModel):
75
+ """Project member information."""
76
+
77
+ user_id: str = Field(..., description="User ID")
78
+ role: ProjectRole = Field(..., description="Role in the project")
79
+ joined_at: datetime = Field(..., description="When user joined")
80
+
81
+ class Config:
82
+ use_enum_values = True
83
+
84
+
85
+ class Project(BaseModel):
86
+ """Project model."""
87
+
88
+ id: str = Field(..., description="Project ID")
89
+ name: str = Field(..., description="Project name")
90
+ description: Optional[str] = Field(None, description="Project description")
91
+ owner_id: str = Field(..., description="Owner user ID")
92
+ members: List[ProjectMember] = Field(default_factory=list, description="Project members")
93
+ item_count: int = Field(default=0, description="Number of items")
94
+ webhook_count: int = Field(default=0, description="Number of webhooks")
95
+ created_at: datetime = Field(..., description="Creation time")
96
+ updated_at: datetime = Field(..., description="Last update time")
97
+
98
+ class Config:
99
+ use_enum_values = True
100
+
101
+
102
+ class ItemContent(BaseModel):
103
+ """Content of an item."""
104
+
105
+ type: ContentType = Field(..., description="Content type")
106
+ data: Dict[str, Any] = Field(default_factory=dict, description="Content data")
107
+ text: Optional[str] = Field(None, description="Text representation")
108
+ url: Optional[str] = Field(None, description="URL for media content")
109
+
110
+ class Config:
111
+ use_enum_values = True
112
+
113
+
114
+ class Decision(BaseModel):
115
+ """Decision on an item."""
116
+
117
+ id: str = Field(..., description="Decision ID")
118
+ type: DecisionType = Field(..., description="Decision type")
119
+ decided_by: str = Field(..., description="User ID who decided")
120
+ decided_at: datetime = Field(..., description="When decision was made")
121
+ comment: Optional[str] = Field(None, description="Optional comment")
122
+
123
+ class Config:
124
+ use_enum_values = True
125
+
126
+
127
+ class ItemVersion(BaseModel):
128
+ """Version history of an item."""
129
+
130
+ id: str = Field(..., description="Version ID")
131
+ item_id: str = Field(..., description="Item ID")
132
+ version_number: int = Field(..., description="Version number")
133
+ content: ItemContent = Field(..., description="Content at this version")
134
+ created_at: datetime = Field(..., description="When this version was created")
135
+
136
+ class Config:
137
+ use_enum_values = True
138
+
139
+
140
+ class Item(BaseModel):
141
+ """Item model."""
142
+
143
+ id: str = Field(..., description="Item ID")
144
+ project_id: str = Field(..., description="Project ID")
145
+ title: str = Field(..., description="Item title")
146
+ content: ItemContent = Field(..., description="Item content")
147
+ status: ItemStatus = Field(..., description="Current status")
148
+ decisions: List[Decision] = Field(default_factory=list, description="Decisions made on this item")
149
+ version: int = Field(default=1, description="Current version number")
150
+ created_by: str = Field(..., description="User ID who created")
151
+ created_at: datetime = Field(..., description="Creation time")
152
+ updated_at: datetime = Field(..., description="Last update time")
153
+ processed_at: Optional[datetime] = Field(None, description="When item was processed")
154
+
155
+ class Config:
156
+ use_enum_values = True
157
+
158
+
159
+ class Webhook(BaseModel):
160
+ """Webhook model."""
161
+
162
+ id: str = Field(..., description="Webhook ID")
163
+ project_id: str = Field(..., description="Project ID")
164
+ name: str = Field(..., description="Webhook name")
165
+ url: str = Field(..., description="Webhook URL")
166
+ type: str = Field(..., description="Webhook type (user or dynamic)")
167
+ events: List[WebhookEvent] = Field(..., description="Events this webhook subscribes to")
168
+ active: bool = Field(default=True, description="Whether webhook is active")
169
+ integration_provider: Optional[str] = Field(None, description="Integration provider")
170
+ created_at: datetime = Field(..., description="Creation time")
171
+ updated_at: datetime = Field(..., description="Last update time")
172
+
173
+ class Config:
174
+ use_enum_values = True
175
+
176
+
177
+ class ApiKey(BaseModel):
178
+ """API key model."""
179
+
180
+ id: str = Field(..., description="API key ID")
181
+ name: str = Field(..., description="Key name")
182
+ key: Optional[str] = Field(None, description="The actual key (only on creation)")
183
+ last_used_at: Optional[datetime] = Field(None, description="Last time key was used")
184
+ created_at: datetime = Field(..., description="Creation time")
185
+
186
+ class Config:
187
+ use_enum_values = True
188
+
189
+
190
+ class PaginatedResponse(BaseModel):
191
+ """Paginated response wrapper."""
192
+
193
+ items: List[Dict[str, Any]] = Field(default_factory=list, description="Items in this page")
194
+ total: int = Field(default=0, description="Total number of items")
195
+ page: int = Field(default=1, description="Current page number")
196
+ per_page: int = Field(default=20, description="Items per page")
197
+ has_more: bool = Field(default=False, description="Whether more items exist")
198
+
199
+ class Config:
200
+ use_enum_values = True
201
+
202
+
203
+ class ErrorResponse(BaseModel):
204
+ """Error response model."""
205
+
206
+ error: str = Field(..., description="Error message")
207
+ code: Optional[str] = Field(None, description="Error code")
208
+ details: Optional[Dict[str, Any]] = Field(None, description="Additional details")
209
+
210
+ class Config:
211
+ use_enum_values = True
swipeflow_api/py.typed ADDED
File without changes