Skip to content

Commit 6be8e15

Browse files
committed
feat(sdk): init http client, impl config & query serialization
1 parent 2abe6e0 commit 6be8e15

3 files changed

Lines changed: 215 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,14 @@ response = tca.fetch_openapi()
530530
schema = response["data"] # The OpenAPI schema
531531
```
532532

533-
## License
533+
## 🔗 Links
534+
535+
- [The Companies API](https://www.thecompaniesapi.com)
536+
- [API Documentation](https://www.thecompaniesapi.com/api)
537+
- [TypeScript SDK](https://github.com/thecompaniesapi/sdk-typescript)
538+
- [Support & Live Chat](https://www.thecompaniesapi.com/)
539+
540+
## 📄 License
534541

535542
[MIT](./LICENSE) License © [TheCompaniesAPI](https://github.com/thecompaniesapi)
536543

src/thecompaniesapi/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .sdk import Client, HttpClient, ApiError
2+
3+
__all__ = ['Client', 'HttpClient', 'ApiError']

src/thecompaniesapi/sdk.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import json
2+
import urllib.parse
3+
from typing import Any, Dict, Optional
4+
import requests
5+
from requests.adapters import HTTPAdapter
6+
from urllib3.util.retry import Retry
7+
8+
9+
class HttpClient:
10+
"""
11+
Base HTTP client for The Companies API.
12+
Handles authentication, request serialization, and response handling.
13+
"""
14+
15+
def __init__(
16+
self,
17+
api_token: Optional[str] = None,
18+
api_url: str = "https://api.thecompaniesapi.com",
19+
visitor_id: Optional[str] = None,
20+
timeout: int = 300
21+
):
22+
self.api_token = api_token
23+
self.api_url = api_url.rstrip('/')
24+
self.visitor_id = visitor_id
25+
self.timeout = timeout
26+
27+
# Create session with retry strategy
28+
self.session = requests.Session()
29+
30+
# Configure retry strategy
31+
retry_strategy = Retry(
32+
total=3,
33+
status_forcelist=[429, 500, 502, 503, 504],
34+
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"],
35+
backoff_factor=1
36+
)
37+
38+
adapter = HTTPAdapter(max_retries=retry_strategy)
39+
self.session.mount("http://", adapter)
40+
self.session.mount("https://", adapter)
41+
42+
# Set default headers
43+
self._setup_default_headers()
44+
45+
def _setup_default_headers(self) -> None:
46+
"""Setup default headers for all requests."""
47+
self.session.headers.update({
48+
'Content-Type': 'application/json',
49+
'Accept': 'application/json',
50+
'User-Agent': 'thecompaniesapi-python-sdk/0.0.1'
51+
})
52+
53+
# Add authorization if token provided
54+
if self.api_token:
55+
self.session.headers['Authorization'] = f'Basic {self.api_token}'
56+
57+
# Add visitor ID if provided
58+
if self.visitor_id:
59+
self.session.headers['Tca-Visitor-Id'] = self.visitor_id
60+
61+
def _serialize_query_params(self, params: Dict[str, Any]) -> Dict[str, str]:
62+
"""
63+
Serialize query parameters, converting objects and arrays to JSON strings.
64+
"""
65+
serialized = {}
66+
67+
for key, value in params.items():
68+
if value is None:
69+
continue
70+
elif isinstance(value, (dict, list)):
71+
# Convert objects and arrays to JSON strings and URL encode them
72+
# This matches: encodeURIComponent(JSON.stringify(query[key]))
73+
json_str = json.dumps(value, separators=(',', ':')) # Compact JSON
74+
serialized[key] = urllib.parse.quote(json_str)
75+
elif isinstance(value, bool):
76+
# Convert boolean to lowercase string
77+
serialized[key] = str(value).lower()
78+
else:
79+
# Convert everything else to string
80+
serialized[key] = str(value)
81+
82+
return serialized
83+
84+
def _prepare_url(self, path: str) -> str:
85+
"""Prepare the full URL for a request."""
86+
# Ensure path starts with /
87+
if not path.startswith('/'):
88+
path = f'/{path}'
89+
90+
return f'{self.api_url}{path}'
91+
92+
def _make_request(
93+
self,
94+
method: str,
95+
path: str,
96+
params: Optional[Dict[str, Any]] = None,
97+
json_data: Optional[Dict[str, Any]] = None,
98+
headers: Optional[Dict[str, str]] = None
99+
) -> Dict[str, Any]:
100+
"""
101+
Make an HTTP request with proper error handling and response parsing.
102+
"""
103+
url = self._prepare_url(path)
104+
105+
# Prepare query parameters
106+
query_params = None
107+
if params:
108+
query_params = self._serialize_query_params(params)
109+
110+
# Prepare request headers
111+
request_headers = {}
112+
if headers:
113+
request_headers.update(headers)
114+
115+
try:
116+
response = self.session.request(
117+
method=method.upper(),
118+
url=url,
119+
params=query_params,
120+
json=json_data,
121+
headers=request_headers,
122+
timeout=self.timeout
123+
)
124+
125+
# Raise an exception for bad status codes
126+
response.raise_for_status()
127+
128+
# Try to parse JSON response
129+
try:
130+
return response.json()
131+
except json.JSONDecodeError:
132+
# If response is not JSON, return text content
133+
return {'data': response.text, 'status': response.status_code}
134+
135+
except requests.exceptions.RequestException as e:
136+
# Handle request errors
137+
raise ApiError(f"Request failed: {str(e)}") from e
138+
139+
def get(
140+
self,
141+
path: str,
142+
params: Optional[Dict[str, Any]] = None,
143+
headers: Optional[Dict[str, str]] = None
144+
) -> Dict[str, Any]:
145+
"""Make a GET request."""
146+
return self._make_request('GET', path, params=params, headers=headers)
147+
148+
def post(
149+
self,
150+
path: str,
151+
json_data: Optional[Dict[str, Any]] = None,
152+
params: Optional[Dict[str, Any]] = None,
153+
headers: Optional[Dict[str, str]] = None
154+
) -> Dict[str, Any]:
155+
"""Make a POST request."""
156+
return self._make_request('POST', path, params=params, json_data=json_data, headers=headers)
157+
158+
def put(
159+
self,
160+
path: str,
161+
json_data: Optional[Dict[str, Any]] = None,
162+
params: Optional[Dict[str, Any]] = None,
163+
headers: Optional[Dict[str, str]] = None
164+
) -> Dict[str, Any]:
165+
"""Make a PUT request."""
166+
return self._make_request('PUT', path, params=params, json_data=json_data, headers=headers)
167+
168+
def delete(
169+
self,
170+
path: str,
171+
params: Optional[Dict[str, Any]] = None,
172+
headers: Optional[Dict[str, str]] = None
173+
) -> Dict[str, Any]:
174+
"""Make a DELETE request."""
175+
return self._make_request('DELETE', path, params=params, headers=headers)
176+
177+
178+
class ApiError(Exception):
179+
"""Custom exception for API errors."""
180+
pass
181+
182+
183+
class Client:
184+
"""
185+
Main client for The Companies API.
186+
This will be extended with operation methods generated from the OpenAPI schema.
187+
"""
188+
189+
def __init__(
190+
self,
191+
api_token: Optional[str] = None,
192+
api_url: str = "https://api.thecompaniesapi.com",
193+
visitor_id: Optional[str] = None,
194+
timeout: int = 300
195+
):
196+
if not api_token:
197+
raise ValueError("api_token is required")
198+
199+
self.http = HttpClient(
200+
api_token=api_token,
201+
api_url=api_url,
202+
visitor_id=visitor_id,
203+
timeout=timeout
204+
)

0 commit comments

Comments
 (0)