clappia-api-tools 0.1.5__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.
- clappia_api_tools-0.1.5.dist-info/METADATA +217 -0
- clappia_api_tools-0.1.5.dist-info/RECORD +27 -0
- clappia_api_tools-0.1.5.dist-info/WHEEL +5 -0
- clappia_api_tools-0.1.5.dist-info/licenses/LICENSE +0 -0
- clappia_api_tools-0.1.5.dist-info/top_level.txt +1 -0
- clappia_tools/__init__.py +17 -0
- clappia_tools/_enums/__init__.py +0 -0
- clappia_tools/_enums/enums.py +0 -0
- clappia_tools/_models/__init__.py +0 -0
- clappia_tools/_models/model.py +40 -0
- clappia_tools/_utils/__init__.py +5 -0
- clappia_tools/_utils/api_utils.py +141 -0
- clappia_tools/_utils/logging_utils.py +132 -0
- clappia_tools/_utils/validators.py +129 -0
- clappia_tools/client/__init__.py +7 -0
- clappia_tools/client/app_definition_client.py +82 -0
- clappia_tools/client/app_management_client.py +508 -0
- clappia_tools/client/base_client.py +27 -0
- clappia_tools/client/clappia_client.py +204 -0
- clappia_tools/client/submission_client.py +301 -0
- clappia_tools/tests/__init__.py +0 -0
- clappia_tools/tests/integration/__init__.py +0 -0
- clappia_tools/tests/integration/definition/test_tools_definition.py +425 -0
- clappia_tools/tests/integration/submission/test_tools_submission.py +263 -0
- clappia_tools/tests/unit/__init__.py +0 -0
- clappia_tools/tests/unit/definition/test_validators.py +177 -0
- clappia_tools/tests/unit/submission/test_client.py +262 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clappia-api-tools
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Python client for Clappia API integration
|
|
5
|
+
Author-email: Rishabh Verma <rishabh.v@clappia.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/clappia-dev/clappia-tools
|
|
8
|
+
Project-URL: Documentation, https://github.com/clappia-dev/clappia-tools#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/clappia-dev/clappia-tools.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/clappia-dev/clappia-tools/issues
|
|
11
|
+
Keywords: clappia,client,mcp,rest-apis,tools,workchat,agents
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: requests>=2.31.0
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: typing-extensions>=4.8.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "test"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: mkdocs>=1.4.0; extra == "docs"
|
|
39
|
+
Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# Clappia Tools
|
|
43
|
+
|
|
44
|
+
**LangChain integration for Clappia API**
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/clappia-tools/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Overview
|
|
52
|
+
|
|
53
|
+
Clappia Tools is a Python package that provides a unified client and a set of tools for seamless integration with the [Clappia API](https://www.clappia.com/). It enables developers to automate workflows, manage submissions, and interact with Clappia apps programmatically. The package is designed for use in automation, data integration, and agent-based systems (e.g., LangChain agents).
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Unified API Client**: One client for all Clappia API operations.
|
|
60
|
+
- **Submission Management**: Create, edit, update owners, and change status of submissions.
|
|
61
|
+
- **App Definition Retrieval**: Fetch complete app structure and metadata.
|
|
62
|
+
- **Input Validation**: Built-in validation for IDs, emails, and status objects.
|
|
63
|
+
- **Extensible Tools**: Modular functions for each operation, easily integrated into agents or scripts.
|
|
64
|
+
- **Comprehensive Testing**: Includes unit and integration tests.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install clappia-tools
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or, for development:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/clappia-dev/clappia-tools.git
|
|
78
|
+
cd clappia-tools
|
|
79
|
+
pip install -e .[dev]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
You must provide your Clappia API credentials and workspace information directly when initializing the `ClappiaClient`:
|
|
87
|
+
|
|
88
|
+
- `api_key`: Your Clappia API key
|
|
89
|
+
- `base_url`: The base URL for the Clappia API (e.g., `https://api.clappia.com`)
|
|
90
|
+
- `workplace_id`: Your Clappia workplace ID
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from clappia_tools import ClappiaClient
|
|
96
|
+
|
|
97
|
+
client = ClappiaClient(
|
|
98
|
+
api_key="your-api-key",
|
|
99
|
+
base_url="https://api.clappia.com",
|
|
100
|
+
workplace_id="your-workplace-id"
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
### Basic Client Usage
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from clappia_tools import ClappiaClient
|
|
111
|
+
|
|
112
|
+
client = ClappiaClient(
|
|
113
|
+
api_key="your-api-key",
|
|
114
|
+
base_url="https://api.clappia.com",
|
|
115
|
+
workplace_id="your-workplace-id"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Create a submission
|
|
119
|
+
result = client.create_submission(
|
|
120
|
+
app_id="MFX093412",
|
|
121
|
+
data={"employee_name": "John Doe", "department": "Engineering"},
|
|
122
|
+
email="user@example.com"
|
|
123
|
+
)
|
|
124
|
+
print(result)
|
|
125
|
+
|
|
126
|
+
# Edit a submission
|
|
127
|
+
result = client.edit_submission(
|
|
128
|
+
app_id="MFX093412",
|
|
129
|
+
submission_id="HGO51464561",
|
|
130
|
+
data={"department": "Marketing"},
|
|
131
|
+
email="user@example.com"
|
|
132
|
+
)
|
|
133
|
+
print(result)
|
|
134
|
+
|
|
135
|
+
# Get app definition
|
|
136
|
+
result = client.get_app_definition(app_id="MFX093412")
|
|
137
|
+
print(result)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Tool Functions
|
|
141
|
+
|
|
142
|
+
You can also use the modular tool functions directly:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from clappia_tools._tools import (
|
|
146
|
+
create_clappia_submission,
|
|
147
|
+
edit_clappia_submission,
|
|
148
|
+
get_app_definition,
|
|
149
|
+
update_clappia_submission_owners,
|
|
150
|
+
update_clappia_submission_status,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Create a submission
|
|
154
|
+
data = {"employee_name": "Jane Doe", "department": "HR"}
|
|
155
|
+
response = create_clappia_submission("MFX093412", data, "user@example.com")
|
|
156
|
+
print(response)
|
|
157
|
+
|
|
158
|
+
# Update submission owners
|
|
159
|
+
response = update_clappia_submission_owners(
|
|
160
|
+
"MFX093412", "HGO51464561", "admin@example.com", ["user1@company.com", "user2@company.com"]
|
|
161
|
+
)
|
|
162
|
+
print(response)
|
|
163
|
+
|
|
164
|
+
# Update submission status
|
|
165
|
+
response = update_clappia_submission_status(
|
|
166
|
+
"MFX093412", "HGO51464561", "admin@example.com", {"statusName": "Approved", "comments": "Reviewed."}
|
|
167
|
+
)
|
|
168
|
+
print(response)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Available Tools
|
|
174
|
+
|
|
175
|
+
- `create_clappia_submission(app_id, data, email)`
|
|
176
|
+
- `edit_clappia_submission(app_id, submission_id, data, email)`
|
|
177
|
+
- `get_app_definition(app_id, language="en", strip_html=True, include_tags=True)`
|
|
178
|
+
- `update_clappia_submission_owners(app_id, submission_id, requesting_user_email_address, email_ids)`
|
|
179
|
+
- `update_clappia_submission_status(app_id, submission_id, requesting_user_email_address, status)`
|
|
180
|
+
|
|
181
|
+
See docstrings in each tool for detailed argument and return value descriptions.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Input Validation
|
|
186
|
+
|
|
187
|
+
- **App ID**: Must be uppercase letters and numbers (e.g., `MFX093412`).
|
|
188
|
+
- **Submission ID**: Must be uppercase letters and numbers (e.g., `HGO51464561`).
|
|
189
|
+
- **Email**: Must be a valid email address.
|
|
190
|
+
- **Status**: Must be a dictionary with a non-empty `statusName` or `name` field.
|
|
191
|
+
|
|
192
|
+
Invalid inputs will return descriptive error messages.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Testing
|
|
197
|
+
|
|
198
|
+
Run all tests (unit and integration):
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pytest
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
|
|
208
|
+
1. Fork the repository and create your branch.
|
|
209
|
+
2. Write clear, well-documented code and tests.
|
|
210
|
+
3. Run `pytest` and ensure all tests pass.
|
|
211
|
+
4. Submit a pull request with a clear description of your changes.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
clappia_api_tools-0.1.5.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
clappia_tools/__init__.py,sha256=p5I5RBY9Y6POnuOf4yKbea_yQ0CD7fTUz4JicjbwvBA,519
|
|
3
|
+
clappia_tools/_enums/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
clappia_tools/_enums/enums.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
clappia_tools/_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
clappia_tools/_models/model.py,sha256=mIXynyUeIK9KrAcDNKRkAFQExr7lZTAm4Rs_LPveZSU,897
|
|
7
|
+
clappia_tools/_utils/__init__.py,sha256=X7W407Kjb4fCc7OcWqex6X_P893k4qLKi-pdgd-6z60,136
|
|
8
|
+
clappia_tools/_utils/api_utils.py,sha256=_J6e1rdAcbndQeQ9i0QsqpdkwHYR5oKdvdAbBcT0OCg,4711
|
|
9
|
+
clappia_tools/_utils/logging_utils.py,sha256=OAZTKJKBPSXFMMViSXVji2FwJ4mY_jGiMZJ9DAdJY8s,3097
|
|
10
|
+
clappia_tools/_utils/validators.py,sha256=W5yIJnr241ZzPg3LyJerKZs8W0Lj7OzLliKbKXayZqE,4866
|
|
11
|
+
clappia_tools/client/__init__.py,sha256=XQbzWq6BDOWs_7iPHBEYBbh55lhzg1Yw4f_xsIbPWFQ,359
|
|
12
|
+
clappia_tools/client/app_definition_client.py,sha256=ITY_hKD_9yuWvk7Ol-oE2GMtLYDUqHzvWudqGcBq3eg,3789
|
|
13
|
+
clappia_tools/client/app_management_client.py,sha256=7ij7lFKCr5NQ5tFJi65n1zHAEhZwxsxEMGXlL2zyz_4,23835
|
|
14
|
+
clappia_tools/client/base_client.py,sha256=0_R_cFPSCqjNPcG0gHPgBwviUOxlkfSIwVQlgiCi2so,833
|
|
15
|
+
clappia_tools/client/clappia_client.py,sha256=h2zSqhSFHB4Ijh-RaxiRg1CXfyxXWEgNwqj92FJ1N5k,10007
|
|
16
|
+
clappia_tools/client/submission_client.py,sha256=jviQdE40qbqFEhWNX-ToHAvMoy-nqjAGkJQef4D3ihY,14494
|
|
17
|
+
clappia_tools/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
clappia_tools/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
clappia_tools/tests/integration/definition/test_tools_definition.py,sha256=jrzPx8VNPkpqJcfS-NVGAcsEix_u8tWtnwAk_dflJ2A,16572
|
|
20
|
+
clappia_tools/tests/integration/submission/test_tools_submission.py,sha256=vFLQ7T-poPjhvmUvGctsVygqEMbsAyZfpuLroBbTEL0,9796
|
|
21
|
+
clappia_tools/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
clappia_tools/tests/unit/definition/test_validators.py,sha256=d2QdIoL8mKWN0GSIYW5_4SQhNP-mNAeEmQc5rVpOWiE,7821
|
|
23
|
+
clappia_tools/tests/unit/submission/test_client.py,sha256=uEiz1D0qdS-5C86FMzd1VR_Emp7Cedz6CVbIiMJiuZw,10771
|
|
24
|
+
clappia_api_tools-0.1.5.dist-info/METADATA,sha256=m6rGHe5uvy1k1K9pDH-85PDn6lGDhRbqgoVCJWKBF3Y,6427
|
|
25
|
+
clappia_api_tools-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
26
|
+
clappia_api_tools-0.1.5.dist-info/top_level.txt,sha256=q8xOO-1KTRJwcux9LiCzVLbYG6KrlF1uH6bsQFx3kpo,14
|
|
27
|
+
clappia_api_tools-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clappia_tools
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clappia Tools - LangChain integration for Clappia API
|
|
3
|
+
|
|
4
|
+
This package provides a unified client for interacting with Clappia APIs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client.clappia_client import ClappiaClient
|
|
8
|
+
from .client.app_definition_client import AppDefinitionClient
|
|
9
|
+
from .client.app_management_client import AppManagementClient
|
|
10
|
+
from .client.submission_client import SubmissionClient
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.5"
|
|
13
|
+
__all__ = ["ClappiaClient", "AppDefinitionClient", "AppManagementClient", "SubmissionClient"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __dir__():
|
|
17
|
+
return __all__
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Field:
|
|
7
|
+
"""Field model for creating an app"""
|
|
8
|
+
"""
|
|
9
|
+
fieldType: str
|
|
10
|
+
label: str
|
|
11
|
+
options: Optional[List[str]] = None
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
fieldType: str
|
|
15
|
+
label: str
|
|
16
|
+
options: Optional[List[str]] = None
|
|
17
|
+
def to_dict(self) -> dict:
|
|
18
|
+
result = {
|
|
19
|
+
"fieldType": self.fieldType,
|
|
20
|
+
"label": self.label,
|
|
21
|
+
}
|
|
22
|
+
if self.options is not None:
|
|
23
|
+
result["options"] = self.options
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Section:
|
|
28
|
+
"""Section model for creating an app"""
|
|
29
|
+
"""
|
|
30
|
+
sectionName: str
|
|
31
|
+
fields: List[Field]
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
sectionName: str
|
|
35
|
+
fields: List[Field]
|
|
36
|
+
def to_dict(self) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"sectionName": self.sectionName,
|
|
39
|
+
"fields": [field.to_dict() for field in self.fields]
|
|
40
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Optional, Dict, Any, Tuple
|
|
5
|
+
from clappia_tools._utils.logging_utils import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClappiaAPIUtils:
|
|
11
|
+
"""Utilities for Clappia API interactions"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: str,
|
|
16
|
+
base_url: str,
|
|
17
|
+
workplace_id: str,
|
|
18
|
+
timeout: int = 30,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Initialize API utilities with configurable parameters
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
api_key: Clappia API key
|
|
25
|
+
base_url: API base URL
|
|
26
|
+
workplace_id: Workplace ID
|
|
27
|
+
timeout: Request timeout in seconds
|
|
28
|
+
"""
|
|
29
|
+
self.api_key = api_key
|
|
30
|
+
self.base_url = base_url
|
|
31
|
+
self.workplace_id = workplace_id
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
|
|
34
|
+
def validate_environment(self) -> Tuple[bool, str]:
|
|
35
|
+
"""Validate that required configuration is available"""
|
|
36
|
+
if not self.api_key:
|
|
37
|
+
return (
|
|
38
|
+
False,
|
|
39
|
+
"API key is not configured",
|
|
40
|
+
)
|
|
41
|
+
if not self.base_url:
|
|
42
|
+
return (
|
|
43
|
+
False,
|
|
44
|
+
"Base URL is not configured",
|
|
45
|
+
)
|
|
46
|
+
if not self.workplace_id:
|
|
47
|
+
return (
|
|
48
|
+
False,
|
|
49
|
+
"Workplace ID is not configured",
|
|
50
|
+
)
|
|
51
|
+
return True, ""
|
|
52
|
+
|
|
53
|
+
def get_headers(self) -> Dict[str, str]:
|
|
54
|
+
"""Get standard headers for API requests"""
|
|
55
|
+
return {
|
|
56
|
+
"x-api-key": self.api_key,
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"workplaceId": self.workplace_id,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def handle_response(
|
|
62
|
+
self, response: requests.Response
|
|
63
|
+
) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
|
|
64
|
+
"""
|
|
65
|
+
Handle API response and return structured result
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (success: bool, error_message: str, data: dict)
|
|
69
|
+
"""
|
|
70
|
+
if response.status_code == 200:
|
|
71
|
+
try:
|
|
72
|
+
return True, None, response.json()
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
logger.warning(f"Valid response but invalid JSON: {response.text}")
|
|
75
|
+
return True, None, {"raw_response": response.text}
|
|
76
|
+
|
|
77
|
+
error_message = self._format_error_message(response)
|
|
78
|
+
return False, error_message, None
|
|
79
|
+
|
|
80
|
+
def _format_error_message(self, response: requests.Response) -> str:
|
|
81
|
+
"""Format error message from API response"""
|
|
82
|
+
if response.status_code in [400, 401, 403, 404]:
|
|
83
|
+
try:
|
|
84
|
+
error_data = response.json()
|
|
85
|
+
return f"API Error ({response.status_code}): {json.dumps(error_data, indent=2)}"
|
|
86
|
+
except json.JSONDecodeError:
|
|
87
|
+
return f"API Error ({response.status_code}): {response.text}"
|
|
88
|
+
else:
|
|
89
|
+
return f"Unexpected API response ({response.status_code}): {response.text}"
|
|
90
|
+
|
|
91
|
+
def make_request(
|
|
92
|
+
self,
|
|
93
|
+
method: str,
|
|
94
|
+
endpoint: str,
|
|
95
|
+
data: Optional[Dict[str, Any]] = None,
|
|
96
|
+
params: Optional[Dict[str, Any]] = None,
|
|
97
|
+
) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
|
|
98
|
+
"""
|
|
99
|
+
Make HTTP request to Clappia API
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
method: HTTP method (GET, POST, etc.)
|
|
103
|
+
endpoint: API endpoint (will be appended to base_url)
|
|
104
|
+
data: Request body data (for POST/PUT requests)
|
|
105
|
+
params: Query parameters (for GET requests)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Tuple of (success: bool, error_message: str, response_data: dict)
|
|
109
|
+
"""
|
|
110
|
+
env_valid, env_error = self.validate_environment()
|
|
111
|
+
if not env_valid:
|
|
112
|
+
return False, f"Configuration error: {env_error}", None
|
|
113
|
+
|
|
114
|
+
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
115
|
+
headers = self.get_headers()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
logger.info(f"Making {method} request to {url}")
|
|
119
|
+
if data:
|
|
120
|
+
logger.debug(f"Request data: {json.dumps(data, indent=2)}")
|
|
121
|
+
|
|
122
|
+
response = requests.request(
|
|
123
|
+
method=method,
|
|
124
|
+
url=url,
|
|
125
|
+
headers=headers,
|
|
126
|
+
json=data,
|
|
127
|
+
params=params,
|
|
128
|
+
timeout=self.timeout,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
logger.info(f"Response status: {response.status_code}")
|
|
132
|
+
logger.debug(f"Response body: {response.text}")
|
|
133
|
+
|
|
134
|
+
return self.handle_response(response)
|
|
135
|
+
|
|
136
|
+
except requests.exceptions.Timeout:
|
|
137
|
+
return False, f"Request timeout after {self.timeout} seconds", None
|
|
138
|
+
except requests.exceptions.ConnectionError:
|
|
139
|
+
return False, "Connection error - unable to reach Clappia API", None
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return False, f"Unexpected error: {str(e)}", None
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class bcolors:
|
|
7
|
+
HEADER = "\033[95m"
|
|
8
|
+
OKBLUE = "\033[94m"
|
|
9
|
+
OKCYAN = "\033[96m"
|
|
10
|
+
OKGREEN = "\033[92m"
|
|
11
|
+
WARNING = "\033[93m"
|
|
12
|
+
FAIL = "\033[91m"
|
|
13
|
+
ENDC = "\033[0m"
|
|
14
|
+
BOLD = "\033[1m"
|
|
15
|
+
UNDERLINE = "\033[4m"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LogLevel(Enum):
|
|
19
|
+
DEBUG = 0
|
|
20
|
+
INFO = 1
|
|
21
|
+
WARNING = 2
|
|
22
|
+
ERROR = 3
|
|
23
|
+
CRITICAL = 4
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Logger:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str = "Logger",
|
|
30
|
+
level: LogLevel = LogLevel.INFO,
|
|
31
|
+
timestamp_format: str = "%Y-%m-%d %H:%M:%S",
|
|
32
|
+
):
|
|
33
|
+
self.name = name
|
|
34
|
+
self.level = level
|
|
35
|
+
self.timestamp_format = timestamp_format
|
|
36
|
+
|
|
37
|
+
self.colors = {
|
|
38
|
+
LogLevel.DEBUG: bcolors.OKBLUE,
|
|
39
|
+
LogLevel.INFO: bcolors.OKGREEN,
|
|
40
|
+
LogLevel.WARNING: bcolors.WARNING,
|
|
41
|
+
LogLevel.ERROR: bcolors.FAIL,
|
|
42
|
+
LogLevel.CRITICAL: bcolors.HEADER,
|
|
43
|
+
}
|
|
44
|
+
self.reset_color = bcolors.ENDC
|
|
45
|
+
|
|
46
|
+
def _should_log(self, level: LogLevel) -> bool:
|
|
47
|
+
return level.value >= self.level.value
|
|
48
|
+
|
|
49
|
+
def _format_message(self, level: LogLevel, message: str) -> str:
|
|
50
|
+
timestamp = datetime.now().strftime(self.timestamp_format)
|
|
51
|
+
return f"[{timestamp}] [{self.name}] [{level.name}] {message}"
|
|
52
|
+
|
|
53
|
+
def _log(self, level: LogLevel, message: str):
|
|
54
|
+
if not self._should_log(level):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
message_str = str(message)
|
|
58
|
+
formatted_message = self._format_message(level, message_str)
|
|
59
|
+
|
|
60
|
+
supports_color = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
61
|
+
|
|
62
|
+
if supports_color:
|
|
63
|
+
color = self.colors.get(level, "")
|
|
64
|
+
colored_message = f"{color}{formatted_message}{self.reset_color}"
|
|
65
|
+
else:
|
|
66
|
+
colored_message = formatted_message
|
|
67
|
+
|
|
68
|
+
output_stream = (
|
|
69
|
+
sys.stderr if level.value >= LogLevel.WARNING.value else sys.stdout
|
|
70
|
+
)
|
|
71
|
+
print(colored_message, file=output_stream)
|
|
72
|
+
|
|
73
|
+
def debug(self, message: str):
|
|
74
|
+
self._log(LogLevel.DEBUG, message)
|
|
75
|
+
|
|
76
|
+
def info(self, message: str):
|
|
77
|
+
self._log(LogLevel.INFO, message)
|
|
78
|
+
|
|
79
|
+
def warning(self, message: str):
|
|
80
|
+
self._log(LogLevel.WARNING, message)
|
|
81
|
+
|
|
82
|
+
def error(self, message: str):
|
|
83
|
+
self._log(LogLevel.ERROR, message)
|
|
84
|
+
|
|
85
|
+
def critical(self, message: str):
|
|
86
|
+
self._log(LogLevel.CRITICAL, message)
|
|
87
|
+
|
|
88
|
+
def set_level(self, level: LogLevel):
|
|
89
|
+
self.level = level
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_default_logger = Logger()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_logger(name: str = "Logger", level: LogLevel = LogLevel.INFO) -> Logger:
|
|
96
|
+
return Logger(name, level)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def debug(message: str):
|
|
100
|
+
_default_logger.debug(message)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def info(message: str):
|
|
104
|
+
_default_logger.info(message)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def warning(message: str):
|
|
108
|
+
_default_logger.warning(message)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def error(message: str):
|
|
112
|
+
_default_logger.error(message)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def critical(message: str):
|
|
116
|
+
_default_logger.critical(message)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def set_level(level: LogLevel):
|
|
120
|
+
_default_logger.set_level(level)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"get_logger",
|
|
125
|
+
"set_level",
|
|
126
|
+
"debug",
|
|
127
|
+
"info",
|
|
128
|
+
"warning",
|
|
129
|
+
"error",
|
|
130
|
+
"critical",
|
|
131
|
+
"LogLevel",
|
|
132
|
+
]
|