yeonjae-universal-http-api-client 1.0.1__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.
- universal_http_api_client/__init__.py +35 -0
- universal_http_api_client/adapters.py +314 -0
- universal_http_api_client/client.py +572 -0
- universal_http_api_client/exceptions.py +209 -0
- universal_http_api_client/models.py +200 -0
- universal_http_api_client/py.typed +0 -0
- universal_http_api_client/utils.py +40 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/METADATA +77 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/RECORD +11 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/WHEEL +5 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
"""
|
2
|
+
HTTPAPIClient 모듈의 예외 클래스 정의
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional, Dict, Any
|
6
|
+
|
7
|
+
|
8
|
+
class APIError(Exception):
|
9
|
+
"""API 호출 관련 기본 예외"""
|
10
|
+
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
message: str,
|
14
|
+
status_code: Optional[int] = None,
|
15
|
+
response_data: Optional[Dict[str, Any]] = None,
|
16
|
+
platform: Optional[str] = None
|
17
|
+
):
|
18
|
+
super().__init__(message)
|
19
|
+
self.message = message
|
20
|
+
self.status_code = status_code
|
21
|
+
self.response_data = response_data or {}
|
22
|
+
self.platform = platform
|
23
|
+
|
24
|
+
def __str__(self) -> str:
|
25
|
+
parts = [self.message]
|
26
|
+
if self.platform:
|
27
|
+
parts.append(f"Platform: {self.platform}")
|
28
|
+
if self.status_code:
|
29
|
+
parts.append(f"Status: {self.status_code}")
|
30
|
+
return " | ".join(parts)
|
31
|
+
|
32
|
+
|
33
|
+
class RateLimitError(APIError):
|
34
|
+
"""Rate Limit 초과 예외"""
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
message: str = "Rate limit exceeded",
|
39
|
+
retry_after: Optional[int] = None,
|
40
|
+
remaining: int = 0,
|
41
|
+
reset_time: Optional[int] = None,
|
42
|
+
platform: Optional[str] = None
|
43
|
+
):
|
44
|
+
super().__init__(message, 429, platform=platform)
|
45
|
+
self.retry_after = retry_after
|
46
|
+
self.remaining = remaining
|
47
|
+
self.reset_time = reset_time
|
48
|
+
|
49
|
+
def __str__(self) -> str:
|
50
|
+
parts = [self.message]
|
51
|
+
if self.platform:
|
52
|
+
parts.append(f"Platform: {self.platform}")
|
53
|
+
if self.retry_after:
|
54
|
+
parts.append(f"Retry after: {self.retry_after}s")
|
55
|
+
if self.remaining is not None:
|
56
|
+
parts.append(f"Remaining: {self.remaining}")
|
57
|
+
return " | ".join(parts)
|
58
|
+
|
59
|
+
|
60
|
+
class AuthenticationError(APIError):
|
61
|
+
"""인증 관련 예외"""
|
62
|
+
|
63
|
+
def __init__(
|
64
|
+
self,
|
65
|
+
message: str = "Authentication failed",
|
66
|
+
platform: Optional[str] = None
|
67
|
+
):
|
68
|
+
super().__init__(message, 401, platform=platform)
|
69
|
+
|
70
|
+
|
71
|
+
class AuthorizationError(APIError):
|
72
|
+
"""권한 관련 예외"""
|
73
|
+
|
74
|
+
def __init__(
|
75
|
+
self,
|
76
|
+
message: str = "Insufficient permissions",
|
77
|
+
platform: Optional[str] = None
|
78
|
+
):
|
79
|
+
super().__init__(message, 403, platform=platform)
|
80
|
+
|
81
|
+
|
82
|
+
class NotFoundError(APIError):
|
83
|
+
"""리소스를 찾을 수 없음 예외"""
|
84
|
+
|
85
|
+
def __init__(
|
86
|
+
self,
|
87
|
+
message: str = "Resource not found",
|
88
|
+
platform: Optional[str] = None
|
89
|
+
):
|
90
|
+
super().__init__(message, 404, platform=platform)
|
91
|
+
|
92
|
+
|
93
|
+
class ServerError(APIError):
|
94
|
+
"""서버 에러 예외"""
|
95
|
+
|
96
|
+
def __init__(
|
97
|
+
self,
|
98
|
+
message: str = "Internal server error",
|
99
|
+
status_code: int = 500,
|
100
|
+
platform: Optional[str] = None
|
101
|
+
):
|
102
|
+
super().__init__(message, status_code, platform=platform)
|
103
|
+
|
104
|
+
|
105
|
+
class NetworkError(APIError):
|
106
|
+
"""네트워크 관련 예외"""
|
107
|
+
|
108
|
+
def __init__(
|
109
|
+
self,
|
110
|
+
message: str = "Network error occurred",
|
111
|
+
platform: Optional[str] = None
|
112
|
+
):
|
113
|
+
super().__init__(message, platform=platform)
|
114
|
+
|
115
|
+
|
116
|
+
class TimeoutError(APIError):
|
117
|
+
"""타임아웃 예외"""
|
118
|
+
|
119
|
+
def __init__(
|
120
|
+
self,
|
121
|
+
message: str = "Request timeout",
|
122
|
+
timeout: Optional[float] = None,
|
123
|
+
platform: Optional[str] = None
|
124
|
+
):
|
125
|
+
super().__init__(message, platform=platform)
|
126
|
+
self.timeout = timeout
|
127
|
+
|
128
|
+
def __str__(self) -> str:
|
129
|
+
parts = [self.message]
|
130
|
+
if self.platform:
|
131
|
+
parts.append(f"Platform: {self.platform}")
|
132
|
+
if self.timeout:
|
133
|
+
parts.append(f"Timeout: {self.timeout}s")
|
134
|
+
return " | ".join(parts)
|
135
|
+
|
136
|
+
|
137
|
+
class ValidationError(APIError):
|
138
|
+
"""데이터 검증 예외"""
|
139
|
+
|
140
|
+
def __init__(
|
141
|
+
self,
|
142
|
+
message: str = "Validation error",
|
143
|
+
field: Optional[str] = None,
|
144
|
+
value: Optional[Any] = None
|
145
|
+
):
|
146
|
+
super().__init__(message)
|
147
|
+
self.field = field
|
148
|
+
self.value = value
|
149
|
+
|
150
|
+
def __str__(self) -> str:
|
151
|
+
parts = [self.message]
|
152
|
+
if self.field:
|
153
|
+
parts.append(f"Field: {self.field}")
|
154
|
+
if self.value is not None:
|
155
|
+
parts.append(f"Value: {self.value}")
|
156
|
+
return " | ".join(parts)
|
157
|
+
|
158
|
+
|
159
|
+
class CacheError(APIError):
|
160
|
+
"""캐시 관련 예외"""
|
161
|
+
|
162
|
+
def __init__(
|
163
|
+
self,
|
164
|
+
message: str = "Cache operation failed",
|
165
|
+
operation: Optional[str] = None
|
166
|
+
):
|
167
|
+
super().__init__(message)
|
168
|
+
self.operation = operation
|
169
|
+
|
170
|
+
def __str__(self) -> str:
|
171
|
+
parts = [self.message]
|
172
|
+
if self.operation:
|
173
|
+
parts.append(f"Operation: {self.operation}")
|
174
|
+
return " | ".join(parts)
|
175
|
+
|
176
|
+
|
177
|
+
class PlatformNotSupportedError(APIError):
|
178
|
+
"""지원하지 않는 플랫폼 예외"""
|
179
|
+
|
180
|
+
def __init__(
|
181
|
+
self,
|
182
|
+
platform: str,
|
183
|
+
supported_platforms: Optional[list] = None
|
184
|
+
):
|
185
|
+
message = f"Platform '{platform}' is not supported"
|
186
|
+
super().__init__(message, platform=platform)
|
187
|
+
self.supported_platforms = supported_platforms or []
|
188
|
+
|
189
|
+
def __str__(self) -> str:
|
190
|
+
parts = [self.message]
|
191
|
+
if self.supported_platforms:
|
192
|
+
parts.append(f"Supported: {', '.join(self.supported_platforms)}")
|
193
|
+
return " | ".join(parts)
|
194
|
+
|
195
|
+
|
196
|
+
def handle_http_error(status_code: int, message: str, platform: Optional[str] = None) -> APIError:
|
197
|
+
"""HTTP 상태 코드에 따른 적절한 예외 반환"""
|
198
|
+
if status_code == 401:
|
199
|
+
return AuthenticationError(message, platform)
|
200
|
+
elif status_code == 403:
|
201
|
+
return AuthorizationError(message, platform)
|
202
|
+
elif status_code == 404:
|
203
|
+
return NotFoundError(message, platform)
|
204
|
+
elif status_code == 429:
|
205
|
+
return RateLimitError(message, platform=platform)
|
206
|
+
elif 500 <= status_code < 600:
|
207
|
+
return ServerError(message, status_code, platform)
|
208
|
+
else:
|
209
|
+
return APIError(message, status_code, platform=platform)
|
@@ -0,0 +1,200 @@
|
|
1
|
+
"""
|
2
|
+
HTTPAPIClient 모듈의 데이터 모델 정의
|
3
|
+
"""
|
4
|
+
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import Dict, Any, Optional, Union, List
|
7
|
+
from datetime import datetime
|
8
|
+
from enum import Enum
|
9
|
+
|
10
|
+
|
11
|
+
class HTTPMethod(Enum):
|
12
|
+
"""HTTP 메서드 정의"""
|
13
|
+
GET = "GET"
|
14
|
+
POST = "POST"
|
15
|
+
PUT = "PUT"
|
16
|
+
DELETE = "DELETE"
|
17
|
+
PATCH = "PATCH"
|
18
|
+
|
19
|
+
|
20
|
+
class Platform(Enum):
|
21
|
+
"""지원하는 플랫폼 정의"""
|
22
|
+
GITHUB = "github"
|
23
|
+
GITLAB = "gitlab"
|
24
|
+
BITBUCKET = "bitbucket"
|
25
|
+
GENERIC = "generic"
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class PlatformConfig:
|
30
|
+
"""플랫폼별 설정"""
|
31
|
+
name: str
|
32
|
+
base_url: str
|
33
|
+
auth_header: str
|
34
|
+
auth_prefix: str = ""
|
35
|
+
rate_limit_per_hour: int = 5000
|
36
|
+
rate_limit_per_minute: Optional[int] = None
|
37
|
+
default_headers: Dict[str, str] = field(default_factory=dict)
|
38
|
+
timeout: int = 30
|
39
|
+
|
40
|
+
def get_auth_header(self, token: str) -> Dict[str, str]:
|
41
|
+
"""인증 헤더 생성"""
|
42
|
+
if self.auth_prefix:
|
43
|
+
auth_value = f"{self.auth_prefix} {token}"
|
44
|
+
else:
|
45
|
+
auth_value = token
|
46
|
+
return {self.auth_header: auth_value}
|
47
|
+
|
48
|
+
|
49
|
+
@dataclass
|
50
|
+
class APIRequest:
|
51
|
+
"""API 요청 정보"""
|
52
|
+
platform: Platform
|
53
|
+
method: HTTPMethod
|
54
|
+
endpoint: str
|
55
|
+
headers: Optional[Dict[str, str]] = None
|
56
|
+
params: Optional[Dict[str, Any]] = None
|
57
|
+
data: Optional[Dict[str, Any]] = None
|
58
|
+
timeout: Optional[int] = None
|
59
|
+
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
61
|
+
"""딕셔너리 변환"""
|
62
|
+
return {
|
63
|
+
"platform": self.platform.value,
|
64
|
+
"method": self.method.value,
|
65
|
+
"endpoint": self.endpoint,
|
66
|
+
"headers": self.headers or {},
|
67
|
+
"params": self.params or {},
|
68
|
+
"data": self.data or {},
|
69
|
+
"timeout": self.timeout
|
70
|
+
}
|
71
|
+
|
72
|
+
|
73
|
+
@dataclass
|
74
|
+
class APIResponse:
|
75
|
+
"""API 응답 정보"""
|
76
|
+
status_code: int
|
77
|
+
data: Dict[str, Any]
|
78
|
+
headers: Dict[str, str]
|
79
|
+
success: bool
|
80
|
+
error_message: Optional[str] = None
|
81
|
+
response_time: Optional[float] = None
|
82
|
+
cached: bool = False
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def success_response(
|
86
|
+
cls,
|
87
|
+
status_code: int,
|
88
|
+
data: Dict[str, Any],
|
89
|
+
headers: Dict[str, str],
|
90
|
+
response_time: Optional[float] = None,
|
91
|
+
cached: bool = False
|
92
|
+
) -> 'APIResponse':
|
93
|
+
"""성공 응답 생성"""
|
94
|
+
return cls(
|
95
|
+
status_code=status_code,
|
96
|
+
data=data,
|
97
|
+
headers=headers,
|
98
|
+
success=True,
|
99
|
+
response_time=response_time,
|
100
|
+
cached=cached
|
101
|
+
)
|
102
|
+
|
103
|
+
@classmethod
|
104
|
+
def error_response(
|
105
|
+
cls,
|
106
|
+
status_code: int,
|
107
|
+
error_message: str,
|
108
|
+
headers: Optional[Dict[str, str]] = None
|
109
|
+
) -> 'APIResponse':
|
110
|
+
"""에러 응답 생성"""
|
111
|
+
return cls(
|
112
|
+
status_code=status_code,
|
113
|
+
data={},
|
114
|
+
headers=headers or {},
|
115
|
+
success=False,
|
116
|
+
error_message=error_message
|
117
|
+
)
|
118
|
+
|
119
|
+
|
120
|
+
@dataclass
|
121
|
+
class RateLimitInfo:
|
122
|
+
"""Rate Limit 정보"""
|
123
|
+
remaining: int
|
124
|
+
limit: int
|
125
|
+
reset_time: datetime
|
126
|
+
retry_after: Optional[int] = None
|
127
|
+
|
128
|
+
@property
|
129
|
+
def is_exhausted(self) -> bool:
|
130
|
+
"""Rate limit 고갈 여부"""
|
131
|
+
return self.remaining <= 0
|
132
|
+
|
133
|
+
def is_near_limit(self, threshold: int = 10) -> bool:
|
134
|
+
"""Rate limit 임계값 근접 여부"""
|
135
|
+
return self.remaining <= threshold
|
136
|
+
|
137
|
+
|
138
|
+
@dataclass
|
139
|
+
class CacheKey:
|
140
|
+
"""캐시 키 정보"""
|
141
|
+
platform: str
|
142
|
+
endpoint: str
|
143
|
+
params_hash: str
|
144
|
+
|
145
|
+
def __str__(self) -> str:
|
146
|
+
return f"{self.platform}:{self.endpoint}:{self.params_hash}"
|
147
|
+
|
148
|
+
|
149
|
+
@dataclass
|
150
|
+
class CachedResponse:
|
151
|
+
"""캐시된 응답 정보"""
|
152
|
+
response: APIResponse
|
153
|
+
cached_at: datetime
|
154
|
+
ttl: int
|
155
|
+
|
156
|
+
@property
|
157
|
+
def is_expired(self) -> bool:
|
158
|
+
"""캐시 만료 여부"""
|
159
|
+
from datetime import datetime, timedelta, timezone
|
160
|
+
return datetime.now(timezone.utc) > self.cached_at + timedelta(seconds=self.ttl)
|
161
|
+
|
162
|
+
|
163
|
+
# 플랫폼별 설정 정의
|
164
|
+
PLATFORM_CONFIGS = {
|
165
|
+
Platform.GITHUB: PlatformConfig(
|
166
|
+
name="GitHub",
|
167
|
+
base_url="https://api.github.com",
|
168
|
+
auth_header="Authorization",
|
169
|
+
auth_prefix="token",
|
170
|
+
rate_limit_per_hour=5000,
|
171
|
+
rate_limit_per_minute=None,
|
172
|
+
default_headers={
|
173
|
+
"Accept": "application/vnd.github.v3+json",
|
174
|
+
"User-Agent": "CodePing-HTTPAPIClient/1.0"
|
175
|
+
}
|
176
|
+
),
|
177
|
+
Platform.GITLAB: PlatformConfig(
|
178
|
+
name="GitLab",
|
179
|
+
base_url="https://gitlab.com/api/v4",
|
180
|
+
auth_header="PRIVATE-TOKEN",
|
181
|
+
auth_prefix="",
|
182
|
+
rate_limit_per_hour=None,
|
183
|
+
rate_limit_per_minute=300,
|
184
|
+
default_headers={
|
185
|
+
"Content-Type": "application/json",
|
186
|
+
"User-Agent": "CodePing-HTTPAPIClient/1.0"
|
187
|
+
}
|
188
|
+
),
|
189
|
+
Platform.BITBUCKET: PlatformConfig(
|
190
|
+
name="Bitbucket",
|
191
|
+
base_url="https://api.bitbucket.org/2.0",
|
192
|
+
auth_header="Authorization",
|
193
|
+
auth_prefix="Bearer",
|
194
|
+
rate_limit_per_hour=1000,
|
195
|
+
default_headers={
|
196
|
+
"Accept": "application/json",
|
197
|
+
"User-Agent": "CodePing-HTTPAPIClient/1.0"
|
198
|
+
}
|
199
|
+
)
|
200
|
+
}
|
File without changes
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""
|
2
|
+
독립적인 유틸리티 모듈
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
from typing import Any, Dict, Optional
|
7
|
+
|
8
|
+
|
9
|
+
class ModuleIOLogger:
|
10
|
+
"""모듈 입출력 로거 - shared.utils.logging.ModuleIOLogger 대체"""
|
11
|
+
|
12
|
+
def __init__(self, module_name: str):
|
13
|
+
self.module_name = module_name
|
14
|
+
self.logger = logging.getLogger(f"ModuleIO.{module_name}")
|
15
|
+
|
16
|
+
def log_input(self, operation: str, **kwargs):
|
17
|
+
"""입력 로깅"""
|
18
|
+
self.logger.debug(f"[{self.module_name}] INPUT {operation}: {kwargs}")
|
19
|
+
|
20
|
+
def log_output(self, operation: str, result: Any, execution_time: Optional[float] = None):
|
21
|
+
"""출력 로깅"""
|
22
|
+
log_msg = f"[{self.module_name}] OUTPUT {operation}: success"
|
23
|
+
if execution_time:
|
24
|
+
log_msg += f" (took {execution_time:.3f}s)"
|
25
|
+
self.logger.debug(log_msg)
|
26
|
+
|
27
|
+
def log_error(self, operation: str, error: Exception, execution_time: Optional[float] = None):
|
28
|
+
"""에러 로깅"""
|
29
|
+
log_msg = f"[{self.module_name}] ERROR {operation}: {type(error).__name__}: {error}"
|
30
|
+
if execution_time:
|
31
|
+
log_msg += f" (took {execution_time:.3f}s)"
|
32
|
+
self.logger.error(log_msg)
|
33
|
+
|
34
|
+
|
35
|
+
def setup_logging(level: str = "INFO") -> None:
|
36
|
+
"""로깅 설정"""
|
37
|
+
logging.basicConfig(
|
38
|
+
level=getattr(logging, level.upper()),
|
39
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
40
|
+
)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: yeonjae-universal-http-api-client
|
3
|
+
Version: 1.0.1
|
4
|
+
Summary: Universal HTTP API client supporting GitHub, GitLab, Bitbucket and other platforms
|
5
|
+
Author-email: "CodePing.AI Team" <contact@codeping.ai>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/yeonjae-work/universal-modules
|
8
|
+
Project-URL: Repository, https://github.com/yeonjae-work/universal-modules
|
9
|
+
Project-URL: Issues, https://github.com/yeonjae-work/universal-modules/issues
|
10
|
+
Project-URL: Changelog, https://github.com/yeonjae-work/universal-modules/blob/main/packages/yeonjae-universal-http-api-client/CHANGELOG.md
|
11
|
+
Keywords: http,api,client,github,gitlab,slack,notion,universal
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
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.8
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
24
|
+
Classifier: Topic :: System :: Networking
|
25
|
+
Requires-Python: >=3.8
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
Requires-Dist: httpx>=0.25.0
|
28
|
+
Requires-Dist: requests>=2.28.0
|
29
|
+
Requires-Dist: pydantic>=2.0.0
|
30
|
+
Requires-Dist: urllib3>=1.26.0
|
31
|
+
Provides-Extra: dev
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
34
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
35
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
36
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
37
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
38
|
+
Requires-Dist: requests-mock>=1.9.0; extra == "dev"
|
39
|
+
|
40
|
+
# Universal HTTP API Client
|
41
|
+
|
42
|
+
Universal HTTP API client with platform-specific adapters for GitHub, GitLab, Slack, and Notion.
|
43
|
+
|
44
|
+
## Features
|
45
|
+
|
46
|
+
- **Multi-Platform Support**: GitHub, GitLab, Slack, Notion APIs
|
47
|
+
- **Smart Caching**: Automatic request caching with configurable TTL
|
48
|
+
- **Rate Limiting**: Built-in rate limiting with platform-specific rules
|
49
|
+
- **Error Handling**: Comprehensive error handling with custom exceptions
|
50
|
+
- **Async Support**: Full async/await support for all operations
|
51
|
+
- **Type Safety**: Complete type annotations with mypy support
|
52
|
+
|
53
|
+
## Installation
|
54
|
+
|
55
|
+
```bash
|
56
|
+
pip install universal-http-api-client
|
57
|
+
```
|
58
|
+
|
59
|
+
## Quick Start
|
60
|
+
|
61
|
+
```python
|
62
|
+
from universal_http_api_client import HTTPAPIClient, Platform
|
63
|
+
|
64
|
+
# GitHub API
|
65
|
+
client = HTTPAPIClient(Platform.GITHUB, "your-token")
|
66
|
+
repo = client.get_repository("owner/repo")
|
67
|
+
client.close()
|
68
|
+
|
69
|
+
# Slack API
|
70
|
+
slack_client = HTTPAPIClient(Platform.SLACK, "your-slack-token")
|
71
|
+
response = slack_client.send_message("channel", "Hello World!")
|
72
|
+
slack_client.close()
|
73
|
+
```
|
74
|
+
|
75
|
+
## License
|
76
|
+
|
77
|
+
MIT License
|
@@ -0,0 +1,11 @@
|
|
1
|
+
universal_http_api_client/__init__.py,sha256=sSioVC1Alh4IBNijvZiEShy8mbh3Xdqimnq5m1F--nI,1056
|
2
|
+
universal_http_api_client/adapters.py,sha256=S_5zyH6NIy-dg_w7v1EMLPBi_KrIRixFMPRxELLZfLc,11699
|
3
|
+
universal_http_api_client/client.py,sha256=lq9jFsO5bku4kkTT0VnqEi5GP2Zx9xqsoeOvk-YlfZQ,21774
|
4
|
+
universal_http_api_client/exceptions.py,sha256=uuABwSQIMnHL92Rdo4Ojw4BNYEfERNsQnVICVzW5tec,5848
|
5
|
+
universal_http_api_client/models.py,sha256=FaOQUitSBEuzbO6KXNiNRqGpYctWX-bBtN2bQB47224,5162
|
6
|
+
universal_http_api_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
universal_http_api_client/utils.py,sha256=PV3Xv-9VOc3OJ9vXHKsVagKOXbE1H66OyOvehHX1gp0,1407
|
8
|
+
yeonjae_universal_http_api_client-1.0.1.dist-info/METADATA,sha256=NkYCWrCWs5R5oEkWPqX4kNsQ2hLVfLawF8EI0HeAdNw,2898
|
9
|
+
yeonjae_universal_http_api_client-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
+
yeonjae_universal_http_api_client-1.0.1.dist-info/top_level.txt,sha256=eiCeozAaUmZUn2d3cu5PPa3fJShjaa1SsxU9wnBmAiI,26
|
11
|
+
yeonjae_universal_http_api_client-1.0.1.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
universal_http_api_client
|