qing-client 0.1.5__tar.gz

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,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: qing-client
3
+ Version: 0.1.5
4
+ Summary: Python client for audit logging services
5
+ Author-email: xiaoyue9527 <xiaoyue9527@example.com>
6
+ License: MIT
7
+ Project-URL: homepage, https://github.com/yourusername/qing-client
8
+ Project-URL: Bug Tracker, https://github.com/yourusername/qing-client/issues
9
+ Project-URL: Documentation, https://github.com/yourusername/qing-client/wiki
10
+ Keywords: audit,logging,security,monitoring
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.7
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.23.0
17
+ Requires-Dist: pydantic>=2.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0; extra == "dev"
20
+ Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
21
+ Requires-Dist: twine>=4.0.0; extra == "dev"
22
+ Requires-Dist: build>=0.10.0; extra == "dev"
23
+ Requires-Dist: bumpversion>=0.6.0; extra == "dev"
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools_scm]
6
+ # 从文件读取版本号
7
+ version_file = "version.txt"
8
+ # 可选:将版本写入Python文件供代码使用
9
+ write_to = "qingclient/_version.py"
10
+ # 确保构建时使用文件版本而非Git标签
11
+ local_scheme = "no-local-version"
12
+ # 添加回退机制
13
+ fallback_version = "0.0.0" # 必须存在,但会被version.txt覆盖
14
+
15
+ [tool.setuptools]
16
+ packages = ["qingclient"]
17
+ package-dir = {"" = "."}
18
+
19
+ [project]
20
+ name = "qing-client"
21
+ dynamic = ["version"] # 关键:声明版本为动态获取
22
+ description = "Python client for audit logging services"
23
+ readme = "README.md"
24
+ requires-python = ">=3.7"
25
+ authors = [
26
+ {name = "xiaoyue9527", email = "xiaoyue9527@example.com" },
27
+ ]
28
+ license = {text = "MIT"}
29
+ classifiers = [
30
+ "Programming Language :: Python :: 3",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Operating System :: OS Independent",
33
+ ]
34
+ keywords = ["audit", "logging", "security", "monitoring"]
35
+
36
+ dependencies = [
37
+ "httpx>=0.23.0",
38
+ "pydantic>=2.0.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ homepage = "https://github.com/yourusername/qing-client"
43
+ "Bug Tracker" = "https://github.com/yourusername/qing-client/issues"
44
+ Documentation = "https://github.com/yourusername/qing-client/wiki"
45
+
46
+ [project.optional-dependencies]
47
+ dev = [
48
+ "pytest>=7.0",
49
+ "pytest-asyncio>=0.20.0",
50
+ "twine>=4.0.0",
51
+ "build>=0.10.0",
52
+ "bumpversion>=0.6.0"
53
+ ]
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: qing-client
3
+ Version: 0.1.5
4
+ Summary: Python client for audit logging services
5
+ Author-email: xiaoyue9527 <xiaoyue9527@example.com>
6
+ License: MIT
7
+ Project-URL: homepage, https://github.com/yourusername/qing-client
8
+ Project-URL: Bug Tracker, https://github.com/yourusername/qing-client/issues
9
+ Project-URL: Documentation, https://github.com/yourusername/qing-client/wiki
10
+ Keywords: audit,logging,security,monitoring
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.7
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.23.0
17
+ Requires-Dist: pydantic>=2.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0; extra == "dev"
20
+ Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
21
+ Requires-Dist: twine>=4.0.0; extra == "dev"
22
+ Requires-Dist: build>=0.10.0; extra == "dev"
23
+ Requires-Dist: bumpversion>=0.6.0; extra == "dev"
@@ -0,0 +1,21 @@
1
+ pyproject.toml
2
+ setup.py
3
+ ./qingclient/_version.py
4
+ ./qingclient/auth_service.py
5
+ ./qingclient/base_client.py
6
+ ./qingclient/init.py
7
+ ./qingclient/msg_service.py
8
+ ./qingclient/token_service.py
9
+ ./qingclient/user_service.py
10
+ qing_client.egg-info/PKG-INFO
11
+ qing_client.egg-info/SOURCES.txt
12
+ qing_client.egg-info/dependency_links.txt
13
+ qing_client.egg-info/requires.txt
14
+ qing_client.egg-info/top_level.txt
15
+ qingclient/_version.py
16
+ qingclient/auth_service.py
17
+ qingclient/base_client.py
18
+ qingclient/init.py
19
+ qingclient/msg_service.py
20
+ qingclient/token_service.py
21
+ qingclient/user_service.py
@@ -0,0 +1,9 @@
1
+ httpx>=0.23.0
2
+ pydantic>=2.0.0
3
+
4
+ [dev]
5
+ pytest>=7.0
6
+ pytest-asyncio>=0.20.0
7
+ twine>=4.0.0
8
+ build>=0.10.0
9
+ bumpversion>=0.6.0
@@ -0,0 +1 @@
1
+ qingclient
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.5'
32
+ __version_tuple__ = version_tuple = (0, 1, 5)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,36 @@
1
+ from .base_client import BaseClient
2
+ from .types import RequestOptions, LoginResponse
3
+
4
+ class AuthService(BaseClient):
5
+ def __init__(self, config):
6
+ super().__init__(config, 'auth')
7
+
8
+ def login(self, identifier: str, password: str, project_id: int = 0, options: RequestOptions = None) -> LoginResponse:
9
+ body = {
10
+ 'grant_type': 'password',
11
+ 'username': identifier,
12
+ 'password': password
13
+ }
14
+
15
+ headers = {}
16
+ if options and options.headers:
17
+ headers = options.headers.copy()
18
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
19
+
20
+ return self.request('/login', RequestOptions(
21
+ method='POST',
22
+ headers=headers,
23
+ params={'project_id': project_id},
24
+ body=body
25
+ ))
26
+
27
+ def logout(self, token: str, options: RequestOptions = None):
28
+ headers = {}
29
+ if options and options.headers:
30
+ headers = options.headers.copy()
31
+ headers['Authorization'] = f'Bearer {token}'
32
+
33
+ return self.request('/logout', RequestOptions(
34
+ method='POST',
35
+ headers=headers
36
+ ))
@@ -0,0 +1,176 @@
1
+ import requests
2
+ import json
3
+ from typing import Dict, Any, Optional, List
4
+ from .types import UserContext, ClientConfig, RequestOptions, ApiResponse, PaginatedResponse
5
+
6
+ class BaseClient:
7
+ def __init__(self, config: ClientConfig, service_name: str):
8
+ self.config = config
9
+ self.service_name = service_name # 存储为实例变量
10
+ self.user_context = None
11
+ self.token = None
12
+
13
+ # 确定基础URL
14
+ self.base_url = self._get_base_url()
15
+
16
+ # 创建session
17
+ self.session = requests.Session()
18
+ self.session.headers.update({
19
+ 'Content-Type': 'application/json',
20
+ 'Accept': 'application/json'
21
+ })
22
+
23
+ def _get_base_url(self):
24
+ # 使用 self.service_name 而不是直接访问 service_name
25
+ service_urls = {
26
+ 'auth': self.config.auth_service_url,
27
+ 'msg': self.config.msg_service_url,
28
+ 'users': self.config.user_service_url,
29
+ 'file': self.config.file_service_url,
30
+ 'survey': self.config.survey_service_url,
31
+ 'token': self.config.token_service_url,
32
+ }
33
+
34
+ # 检查服务名称是否在支持的列表中
35
+ if self.service_name not in service_urls:
36
+ raise ValueError(f"Unsupported service: {self.service_name}")
37
+
38
+ return service_urls[self.service_name]
39
+
40
+ def set_user_context(self, context: UserContext):
41
+ self.user_context = context
42
+ return self
43
+
44
+ def set_token(self, token: str):
45
+ self.token = token
46
+ return self
47
+
48
+ def _init_user_context_headers(self, context: UserContext) -> Dict[str, str]:
49
+ return {
50
+ 'v-user-id': context.user_id,
51
+ 'v-user-role': context.role,
52
+ 'v-project-id': context.project_id
53
+ }
54
+
55
+ def _handle_api_error(self, error, path):
56
+ error_message = f"[{self.service_name}服务] "
57
+
58
+ if hasattr(error, 'response') and error.response is not None:
59
+ response = error.response
60
+ try:
61
+ response_data = response.json()
62
+ if 'message' in response_data:
63
+ error_message += response_data['message']
64
+ elif 'error' in response_data:
65
+ error_message += response_data['error']
66
+ else:
67
+ error_message += f"请求失败,状态码: {response.status_code}"
68
+ except:
69
+ error_message += f"请求失败,状态码: {response.status_code}"
70
+
71
+ # 添加详细错误信息
72
+ error_message += f" | 状态码: {response.status_code}"
73
+ if 'x-request-id' in response.headers:
74
+ error_message += f" | 请求ID: {response.headers['x-request-id']}"
75
+ else:
76
+ error_message += str(error)
77
+
78
+ # 添加路径信息
79
+ error_message += f" (路径: {path})"
80
+
81
+ raise Exception(error_message)
82
+
83
+ def request(self, path: str, options: RequestOptions = None) -> Any:
84
+ if options is None:
85
+ options = RequestOptions(method='GET')
86
+
87
+ try:
88
+ url = f"{self.base_url}{path}"
89
+
90
+ # 准备请求参数
91
+ headers = options.headers.copy() if options.headers else {}
92
+
93
+ # 添加用户上下文
94
+ context = options.user_context or self.user_context
95
+ if context:
96
+ headers.update(self._init_user_context_headers(context))
97
+
98
+ # 添加认证令牌
99
+ if self.token:
100
+ headers['Authorization'] = f"Bearer {self.token}"
101
+
102
+ # 发送请求
103
+ response = self.session.request(
104
+ method=options.method,
105
+ url=url,
106
+ headers=headers,
107
+ params=options.params,
108
+ json=options.body,
109
+ timeout=30
110
+ )
111
+
112
+ # 检查HTTP状态
113
+ response.raise_for_status()
114
+
115
+ # 解析响应
116
+ response_data = response.json()
117
+
118
+ # 检查业务成功状态
119
+ if not response_data.get('success', False):
120
+ raise Exception(response_data.get('message', '业务请求失败'))
121
+
122
+ # 返回实际数据
123
+ return response_data.get('data')
124
+
125
+ except Exception as error:
126
+ self._handle_api_error(error, path)
127
+
128
+ def paginated_request(self, path: str, options: RequestOptions = None) -> PaginatedResponse:
129
+ if options is None:
130
+ options = RequestOptions(method='GET')
131
+
132
+ try:
133
+ url = f"{self.base_url}{path}"
134
+
135
+ # 准备请求参数
136
+ headers = options.headers.copy() if options.headers else {}
137
+
138
+ # 添加用户上下文
139
+ context = options.user_context or self.user_context
140
+ if context:
141
+ headers.update(self._init_user_context_headers(context))
142
+
143
+ # 添加认证令牌
144
+ if self.token:
145
+ headers['Authorization'] = f"Bearer {self.token}"
146
+
147
+ # 发送请求
148
+ response = self.session.request(
149
+ method=options.method,
150
+ url=url,
151
+ headers=headers,
152
+ params=options.params,
153
+ json=options.body,
154
+ timeout=30
155
+ )
156
+
157
+ # 检查HTTP状态
158
+ response.raise_for_status()
159
+
160
+ # 解析响应
161
+ response_data = response.json()
162
+
163
+ # 检查业务成功状态
164
+ if not response_data.get('success', False):
165
+ raise Exception(response_data.get('message', '业务请求失败'))
166
+
167
+ # 返回分页响应
168
+ return PaginatedResponse(
169
+ data=response_data.get('data', []),
170
+ success=response_data.get('success', False),
171
+ message=response_data.get('message', ''),
172
+ pagination=response_data.get('pagination', {})
173
+ )
174
+
175
+ except Exception as error:
176
+ self._handle_api_error(error, path)
@@ -0,0 +1,21 @@
1
+ from .auth_service import AuthService
2
+ from .msg_service import MsgService
3
+ from .token_service import TokenService
4
+ from .user_service import UserService
5
+ from .base_client import BaseClient
6
+ from .types import *
7
+
8
+ class QingClient:
9
+ def __init__(self, config):
10
+ self.config = config
11
+ self.auth = AuthService(config)
12
+ self.msg = MsgService(config)
13
+ self.token = TokenService(config)
14
+ self.user = UserService(config)
15
+
16
+ def set_user_context(self, context):
17
+ self.auth.set_user_context(context)
18
+ self.msg.set_user_context(context)
19
+ self.token.set_user_context(context)
20
+ self.user.set_user_context(context)
21
+ return self
@@ -0,0 +1,20 @@
1
+ from .base_client import BaseClient
2
+ from .types import RequestOptions, MailRequest, FeishuMessage
3
+
4
+ class MsgService(BaseClient):
5
+ def __init__(self, config):
6
+ super().__init__(config, 'msg')
7
+
8
+ def send_mail(self, request: MailRequest, options: RequestOptions = None):
9
+ return self.request('/mail/send', RequestOptions(
10
+ method='POST',
11
+ body=request,
12
+ headers=options.headers if options else None
13
+ ))
14
+
15
+ def send_feishu_message(self, message: FeishuMessage, options: RequestOptions = None):
16
+ return self.request('/webhook/feishu/send', RequestOptions(
17
+ method='POST',
18
+ body=message,
19
+ headers=options.headers if options else None
20
+ ))
@@ -0,0 +1,34 @@
1
+ from .base_client import BaseClient
2
+ from .types import RequestOptions, WxOfficialAccountTokenResponse, WxJsapiTicketResponse, WxSignatureRequest, WxSignatureResponse, WxMiniProgramTokenResponse
3
+
4
+ class TokenService(BaseClient):
5
+ def __init__(self, config):
6
+ super().__init__(config, 'token')
7
+
8
+ def get_wx_official_account_token(self, appid: str, options: RequestOptions = None) -> WxOfficialAccountTokenResponse:
9
+ return self.request('/wxh5/accesstoken', RequestOptions(
10
+ method='GET',
11
+ params={'appid': appid},
12
+ headers=options.headers if options else None
13
+ ))
14
+
15
+ def get_wx_jsapi_ticket(self, appid: str, options: RequestOptions = None) -> WxJsapiTicketResponse:
16
+ return self.request('/wxh5/jsapi_ticket', RequestOptions(
17
+ method='GET',
18
+ params={'appid': appid},
19
+ headers=options.headers if options else None
20
+ ))
21
+
22
+ def get_wx_signature(self, signature_request: WxSignatureRequest, options: RequestOptions = None) -> WxSignatureResponse:
23
+ return self.request('/wxh5/signature', RequestOptions(
24
+ method='POST',
25
+ body=signature_request,
26
+ headers=options.headers if options else None
27
+ ))
28
+
29
+ def get_wx_mini_program_token(self, appid: str, options: RequestOptions = None) -> WxMiniProgramTokenResponse:
30
+ return self.request('/wxmp/accesstoken', RequestOptions(
31
+ method='GET',
32
+ params={'appid': appid},
33
+ headers=options.headers if options else None
34
+ ))
@@ -0,0 +1,51 @@
1
+ from .base_client import BaseClient
2
+ from .types import RequestOptions, PaginatedResponse, User, UserCreateRequest, UserUpdateRequest
3
+
4
+ class UserService(BaseClient):
5
+ def __init__(self, config):
6
+ super().__init__(config, 'users')
7
+
8
+ def get_current_user(self, options: RequestOptions = None) -> User:
9
+ return self.request('/me', RequestOptions(
10
+ method='GET',
11
+ headers=options.headers if options else None
12
+ ))
13
+
14
+ def create_user(self, user_data: UserCreateRequest, options: RequestOptions = None) -> User:
15
+ return self.request('', RequestOptions(
16
+ method='POST',
17
+ body={
18
+ 'username': user_data.username,
19
+ 'email': user_data.email,
20
+ 'password': user_data.password,
21
+ 'name': user_data.name,
22
+ 'role': user_data.role,
23
+ 'phone': user_data.phone,
24
+ 'project_id': user_data.project_id
25
+ },
26
+ headers=options.headers if options else None
27
+ ))
28
+
29
+ def update_user(self, user_id: int, update_data: UserUpdateRequest, options: RequestOptions = None) -> User:
30
+ return self.request(f'/{user_id}', RequestOptions(
31
+ method='PUT',
32
+ body={
33
+ 'name': update_data.name,
34
+ 'avatar': update_data.avatar,
35
+ 'phone': update_data.phone,
36
+ 'role': update_data.role,
37
+ 'project_id': update_data.project_id
38
+ },
39
+ headers=options.headers if options else None
40
+ ))
41
+
42
+ def list_users(self, include_inactive: bool = False, page: int = 1, per_page: int = 10, options: RequestOptions = None) -> PaginatedResponse[User]:
43
+ return self.paginated_request('', RequestOptions(
44
+ method='GET',
45
+ params={
46
+ 'include_inactive': include_inactive,
47
+ 'page': page,
48
+ 'per_page': per_page
49
+ },
50
+ headers=options.headers if options else None
51
+ ))
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from setuptools import setup
2
+
3
+ if __name__ == "__main__":
4
+ setup()