pixelarraylib 1.0.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,344 @@
1
+ import aiohttp
2
+ import asyncio
3
+ from datetime import datetime, timedelta
4
+ import pandas as pd
5
+ from typing import List, Dict, Optional, Tuple
6
+
7
+
8
+ class GitLabCodeAnalyzer:
9
+ def __init__(self, gitlab_url, private_token):
10
+ self.gitlab_url = gitlab_url.rstrip("/")
11
+ self.headers = {"PRIVATE-TOKEN": private_token}
12
+ self.session = None
13
+
14
+ async def __aenter__(self):
15
+ """异步上下文管理器入口"""
16
+ self.session = aiohttp.ClientSession(headers=self.headers)
17
+ return self
18
+
19
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
20
+ """异步上下文管理器出口"""
21
+ if self.session:
22
+ await self.session.close()
23
+
24
+ async def _get_session(self):
25
+ """获取或创建aiohttp会话"""
26
+ if self.session is None:
27
+ self.session = aiohttp.ClientSession(headers=self.headers)
28
+ return self.session
29
+
30
+ async def _close_session(self):
31
+ """关闭aiohttp会话"""
32
+ if self.session:
33
+ await self.session.close()
34
+ self.session = None
35
+
36
+ async def get_project_id_name_list(self, group_id=None, owned_only=True):
37
+ """获取项目的 ID 和名称列表
38
+
39
+ Args:
40
+ group_id: 组ID,如果指定则只获取该组的项目
41
+ owned_only: 是否只获取用户拥有的项目,默认为True以提高速度
42
+ """
43
+ projects = await self.get_all_projects(
44
+ group_id, owned_only=owned_only, membership_only=True
45
+ )
46
+ return [(p["id"], p["name"]) for p in projects]
47
+
48
+ async def get_all_projects(
49
+ self, group_id=None, owned_only=True, membership_only=True
50
+ ):
51
+ """获取项目列表
52
+
53
+ Args:
54
+ group_id: 组ID,如果指定则只获取该组的项目
55
+ owned_only: 是否只获取用户拥有的项目
56
+ membership_only: 是否只获取用户有权限的项目
57
+ """
58
+ if group_id:
59
+ url = f"{self.gitlab_url}/api/v4/groups/{group_id}/projects"
60
+ else:
61
+ url = f"{self.gitlab_url}/api/v4/projects"
62
+
63
+ # 构建查询参数
64
+ params = {
65
+ "page": 1,
66
+ "per_page": 100,
67
+ "simple": "true", # 只返回基本信息,提高速度
68
+ "order_by": "name",
69
+ "sort": "asc",
70
+ }
71
+
72
+ if owned_only:
73
+ params["owned"] = "true"
74
+ if membership_only:
75
+ params["membership"] = "true"
76
+
77
+ projects = []
78
+ page = 1
79
+ session = await self._get_session()
80
+
81
+ while True:
82
+ params["page"] = page
83
+ async with session.get(url, params=params) as response:
84
+ # 检查响应状态
85
+ if response.status != 200:
86
+ print(f"请求失败,状态码: {response.status}")
87
+ text = await response.text()
88
+ print(f"响应内容: {text}")
89
+ break
90
+
91
+ # 获取响应数据
92
+ data = await response.json()
93
+ print(f"第{page}页数据: {len(data)}个项目")
94
+
95
+ # 如果返回的数据为空,说明已经到达最后一页
96
+ if not data:
97
+ break
98
+
99
+ projects.extend(data)
100
+ page += 1
101
+
102
+ # 如果返回的数据少于per_page,说明这是最后一页
103
+ if len(data) < params["per_page"]:
104
+ break
105
+
106
+ print(f"总共获取到 {len(projects)} 个项目")
107
+ return projects
108
+
109
+ async def get_project_code_stats(self, project_id, since=None, until=None):
110
+ """获取项目的代码统计,包括每个贡献者的提交数和代码行数变化
111
+
112
+ Args:
113
+ project_id: 项目ID
114
+ since: 开始日期,格式为YYYY-MM-DD
115
+ until: 结束日期,格式为YYYY-MM-DD
116
+ """
117
+ session = await self._get_session()
118
+
119
+ # 获取贡献者基本信息
120
+ contributors_url = (
121
+ f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/contributors"
122
+ )
123
+ async with session.get(contributors_url) as response:
124
+ if response.status != 200:
125
+ print(f"获取贡献者信息失败,状态码: {response.status}")
126
+ return []
127
+ contributors = await response.json()
128
+
129
+ # 获取所有提交的统计信息(避免重复计算)
130
+ all_commits_stats = await self._get_all_commits_stats(
131
+ session, project_id, since, until
132
+ )
133
+
134
+ # 为每个贡献者分配统计信息
135
+ for contributor in contributors:
136
+ author_email = contributor.get("email", "")
137
+ author_name = contributor.get("name", "")
138
+
139
+ # 从所有提交中筛选该贡献者的提交
140
+ contributor_commits = [
141
+ commit
142
+ for commit in all_commits_stats
143
+ if commit.get("author_email", "").lower() == author_email.lower()
144
+ ]
145
+
146
+ # 计算该贡献者的统计信息
147
+ # 注意:这里统计的是代码变化量,不是最终代码量
148
+ total_additions = sum(
149
+ commit.get("additions", 0) for commit in contributor_commits
150
+ )
151
+ total_deletions = sum(
152
+ commit.get("deletions", 0) for commit in contributor_commits
153
+ )
154
+
155
+ # 更新贡献者统计信息
156
+ contributor["additions"] = total_additions
157
+ contributor["deletions"] = total_deletions
158
+ contributor["total_changes"] = total_additions + total_deletions
159
+ contributor["commit_details"] = contributor_commits
160
+
161
+ # 添加净变化行数(新增 - 删除)
162
+ contributor["net_changes"] = total_additions - total_deletions
163
+
164
+ return contributors
165
+
166
+ async def _get_all_commits_stats(self, session, project_id, since=None, until=None):
167
+ """获取项目中所有提交的统计信息"""
168
+ commits_url = (
169
+ f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/commits"
170
+ )
171
+ commit_params = {
172
+ "per_page": 100,
173
+ "ref_name": "master", # 只统计master分支的提交
174
+ }
175
+
176
+ if since:
177
+ commit_params["since"] = since
178
+ if until:
179
+ commit_params["until"] = until
180
+
181
+ all_commits = []
182
+ page = 1
183
+
184
+ while True:
185
+ commit_params["page"] = page
186
+ async with session.get(commits_url, params=commit_params) as response:
187
+ if response.status != 200:
188
+ print(f"获取提交列表失败,状态码: {response.status}")
189
+ break
190
+
191
+ commits = await response.json()
192
+ if not commits:
193
+ break
194
+
195
+ all_commits.extend(commits)
196
+ page += 1
197
+
198
+ if len(commits) < commit_params["per_page"]:
199
+ break
200
+
201
+ # 过滤掉合并提交,只保留真正的功能提交
202
+ filtered_commits = []
203
+ for commit in all_commits:
204
+ title = commit.get("title", "").lower()
205
+ message = commit.get("message", "").lower()
206
+
207
+ # 过滤掉合并提交
208
+ if (
209
+ title.startswith("merge branch")
210
+ or title.startswith("merge pull request")
211
+ or "merge" in title
212
+ and "into" in title
213
+ ):
214
+ continue
215
+
216
+ filtered_commits.append(commit)
217
+
218
+ print(f"总提交数: {len(all_commits)}, 过滤后提交数: {len(filtered_commits)}")
219
+
220
+ # 并发获取每个提交的详细统计
221
+ commit_tasks = []
222
+ for commit in filtered_commits:
223
+ task = self._get_commit_details(session, project_id, commit)
224
+ commit_tasks.append(task)
225
+
226
+ # 等待所有提交详情获取完成
227
+ commit_details_list = await asyncio.gather(*commit_tasks)
228
+
229
+ # 过滤掉None值
230
+ return [commit for commit in commit_details_list if commit is not None]
231
+
232
+ async def _get_commit_details(self, session, project_id, commit):
233
+ """获取单个提交的详细信息"""
234
+ commit_id = commit["id"]
235
+ commit_stats_url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/commits/{commit_id}"
236
+
237
+ async with session.get(commit_stats_url) as commit_response:
238
+ if commit_response.status == 200:
239
+ commit_data = await commit_response.json()
240
+ stats = commit_data.get("stats", {})
241
+
242
+ # 返回详细的提交信息
243
+ return {
244
+ "id": commit_id,
245
+ "short_id": commit.get("short_id", ""),
246
+ "title": commit.get("title", ""),
247
+ "message": commit.get("message", ""),
248
+ "author_name": commit.get("author_name", ""),
249
+ "author_email": commit.get("author_email", ""),
250
+ "committed_date": commit.get("committed_date", ""),
251
+ "created_at": commit.get("created_at", ""),
252
+ "additions": stats.get("additions", 0),
253
+ "deletions": stats.get("deletions", 0),
254
+ "total": stats.get("total", 0),
255
+ }
256
+ else:
257
+ return None
258
+
259
+ async def generate_code_report(self, group_id=None, since=None, until=None):
260
+ """生成代码统计报告
261
+
262
+ Args:
263
+ group_id: 组ID,如果指定则只获取该组的项目
264
+ since: 开始日期,格式为YYYY-MM-DD
265
+ until: 结束日期,格式为YYYY-MM-DD
266
+ """
267
+ projects = await self.get_all_projects(group_id)
268
+ report_data = []
269
+
270
+ # 并发处理所有项目
271
+ project_tasks = []
272
+ for project in projects:
273
+ task = self._process_project_for_report(project, since, until)
274
+ project_tasks.append(task)
275
+
276
+ # 等待所有项目处理完成
277
+ project_results = await asyncio.gather(*project_tasks)
278
+
279
+ # 汇总所有项目的数据
280
+ for project_data in project_results:
281
+ report_data.extend(project_data)
282
+
283
+ if not report_data:
284
+ print("没有找到任何贡献者数据")
285
+ return pd.DataFrame()
286
+
287
+ return report_data
288
+
289
+ async def _process_project_for_report(self, project, since=None, until=None):
290
+ """处理单个项目的报告数据"""
291
+ project_id = project["id"]
292
+ project_name = project["name"]
293
+ project_data = []
294
+
295
+ print(f"分析项目: {project_name}")
296
+
297
+ # 获取贡献者统计,包含代码行数信息
298
+ contributors = await self.get_project_code_stats(
299
+ project_id, since=since, until=until
300
+ )
301
+
302
+ for contributor in contributors:
303
+ project_data.append(
304
+ {
305
+ "project": project_name,
306
+ "author": contributor["name"],
307
+ "email": contributor["email"],
308
+ "commits": contributor["commits"],
309
+ "additions": contributor.get("additions", 0),
310
+ "deletions": contributor.get("deletions", 0),
311
+ "total_changes": contributor.get("total_changes", 0),
312
+ "net_changes": contributor.get("net_changes", 0),
313
+ }
314
+ )
315
+
316
+ return project_data
317
+
318
+ async def get_commit_details_report(self, project_id, since=None, until=None):
319
+ """获取详细的提交信息报告
320
+
321
+ Args:
322
+ project_id: 项目ID
323
+ since: 开始日期,格式为YYYY-MM-DD
324
+ until: 结束日期,格式为YYYY-MM-DD
325
+
326
+ Returns:
327
+ List[Dict]: 包含所有提交详细信息的列表
328
+ """
329
+ contributors = await self.get_project_code_stats(
330
+ project_id, since=since, until=until
331
+ )
332
+
333
+ all_commits = []
334
+ for contributor in contributors:
335
+ commit_details = contributor.get("commit_details", [])
336
+ for commit in commit_details:
337
+ commit["contributor_name"] = contributor.get("name", "")
338
+ commit["contributor_email"] = contributor.get("email", "")
339
+ all_commits.append(commit)
340
+
341
+ # 按提交日期排序
342
+ all_commits.sort(key=lambda x: x.get("committed_date", ""), reverse=True)
343
+
344
+ return all_commits
@@ -0,0 +1,61 @@
1
+ import requests
2
+ from typing import List, Optional
3
+
4
+
5
+ class GitLabPyPIPackageManager:
6
+ def __init__(self, project_id: str, token: str, api_version: str = "v4"):
7
+ self.gitlab_url = "https://gitlab.com"
8
+ self.project_id = project_id
9
+ self.token = token
10
+ self.api_version = api_version
11
+ self.base_url = f"{self.gitlab_url}/api/{api_version}/projects/{project_id}"
12
+ self.headers = {
13
+ "Authorization": f"Bearer {token}",
14
+ "Content-Type": "application/json",
15
+ }
16
+
17
+ def _find_package_id(self, package_name: str, package_version: str) -> str:
18
+ url = f"{self.base_url}/packages"
19
+ params = {
20
+ "package_type": "pypi",
21
+ "package_name": package_name,
22
+ "package_version": package_version,
23
+ }
24
+ response = requests.get(url, headers=self.headers, params=params)
25
+ if response.status_code == 200:
26
+ for package in response.json():
27
+ if (
28
+ package["name"] == package_name
29
+ and package["version"] == package_version
30
+ ):
31
+ return package["id"]
32
+ return None
33
+
34
+ def list_packages(self) -> List[str]:
35
+ url = f"{self.base_url}/packages"
36
+ params = {"package_type": "pypi"}
37
+ response = requests.get(url, headers=self.headers, params=params)
38
+ if response.status_code == 200:
39
+ return response.json()
40
+ else:
41
+ return []
42
+
43
+ def list_package_versions(self, package_name: str) -> List[str]:
44
+ url = f"{self.base_url}/packages"
45
+ params = {"package_type": "pypi", "package_name": package_name}
46
+ response = requests.get(url, headers=self.headers, params=params)
47
+ if response.status_code == 200:
48
+ return [version["version"] for version in response.json()]
49
+ else:
50
+ return []
51
+
52
+ def delete_package(self, package_name: str, package_version: str):
53
+ package_id = self._find_package_id(package_name, package_version)
54
+ if package_id is None:
55
+ raise ValueError(f"Package {package_name} {package_version} not found")
56
+ url = f"{self.base_url}/packages/{package_id}"
57
+ response = requests.delete(url, headers=self.headers)
58
+ if response.status_code == 200:
59
+ return True
60
+ else:
61
+ return False
File without changes
@@ -0,0 +1,132 @@
1
+ import requests
2
+ import json
3
+ import asyncio
4
+
5
+
6
+ class Feishu:
7
+ channel_map = {
8
+ "矩阵像素订阅群": "https://open.feishu.cn/open-apis/bot/v2/hook/6e368741-ab2e-46f4-a945-5c1182303f91",
9
+ "devtoolkit服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/b9e7cfa1-c63f-4a9f-9699-286f7316784b",
10
+ "baymax服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/5d8d4fa6-67c4-4202-9122-389d1ec2b668",
11
+ "videodriver服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/5359b92d-02ab-47ca-a617-58b8152eaa2d",
12
+ "arraycut服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/e610b1e8-f867-4670-8d4d-0553f64884f0",
13
+ "knowledgebase微服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/5f007914-235a-4287-8349-6c1dd7c70024",
14
+ "llm微服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/54942aa6-24f1-4851-8fe9-d7c87572d00a",
15
+ "thirdparty微服务报警": "https://open.feishu.cn/open-apis/bot/v2/hook/b1e6237a-1323-4ad9-96f4-d74de5cdc00f",
16
+ }
17
+
18
+ def __init__(self, channel_name):
19
+ self.webhook_url = self.channel_map[channel_name]
20
+
21
+ def send(self, text):
22
+ print(text)
23
+ headers = {"Content-Type": "application/json"}
24
+ data = {"msg_type": "text", "content": {"text": text}}
25
+ response = requests.post(
26
+ self.webhook_url, headers=headers, data=json.dumps(data)
27
+ )
28
+ return bool(
29
+ response
30
+ and response.json().get("StatusCode") == 0
31
+ and response.json().get("StatusMessage") == "success"
32
+ )
33
+
34
+ async def send_async(self, text: str):
35
+ return await asyncio.to_thread(self.send, text)
36
+
37
+ def send_markdown(
38
+ self,
39
+ markdown_content: str,
40
+ title: str,
41
+ template: str = "turquoise",
42
+ ):
43
+ headers = {"Content-Type": "application/json; charset=utf-8"}
44
+ card = {
45
+ "config": {"wide_screen_mode": True, "enable_forward": True},
46
+ "header": {
47
+ "title": {"tag": "plain_text", "content": title},
48
+ "template": template,
49
+ },
50
+ "elements": [
51
+ {"tag": "div", "text": {"tag": "lark_md", "content": markdown_content}}
52
+ ],
53
+ }
54
+ payload = {"msg_type": "interactive", "card": card}
55
+ resp = requests.post(
56
+ self.webhook_url, headers=headers, data=json.dumps(payload)
57
+ )
58
+ return bool(resp.ok and resp.json().get("StatusCode") == 0)
59
+
60
+ async def send_markdown_async(
61
+ self,
62
+ markdown_content: str,
63
+ title: str,
64
+ template: str = "turquoise",
65
+ ):
66
+ return await asyncio.to_thread(
67
+ self.send_markdown, markdown_content, title, template
68
+ )
69
+
70
+ def send_table(
71
+ self,
72
+ headers: list,
73
+ rows: list,
74
+ title: str,
75
+ template: str = "turquoise",
76
+ ):
77
+ headers_req = {"Content-Type": "application/json; charset=utf-8"}
78
+ n_cols = len(headers)
79
+ columns = []
80
+ for ci in range(n_cols):
81
+ elements_in_col = []
82
+ # 表头
83
+ elements_in_col.append(
84
+ {
85
+ "tag": "div",
86
+ "text": {"tag": "lark_md", "content": f"**{headers[ci]}**"},
87
+ }
88
+ )
89
+ # 每一行的该列内容
90
+ for row in rows:
91
+ cell = row[ci] if ci < len(row) else ""
92
+ elements_in_col.append(
93
+ {"tag": "div", "text": {"tag": "lark_md", "content": cell}}
94
+ )
95
+ columns.append(
96
+ {
97
+ "tag": "column",
98
+ "width": "weighted",
99
+ "weight": 1,
100
+ "elements": elements_in_col,
101
+ }
102
+ )
103
+
104
+ elements = [{"tag": "column_set", "columns": columns}]
105
+
106
+ card = {
107
+ "config": {"wide_screen_mode": True, "enable_forward": True},
108
+ "header": {
109
+ "title": {"tag": "plain_text", "content": title},
110
+ "template": template,
111
+ },
112
+ "elements": elements,
113
+ }
114
+ payload = {"msg_type": "interactive", "card": card}
115
+
116
+ resp = requests.post(
117
+ self.webhook_url, headers=headers_req, data=json.dumps(payload)
118
+ )
119
+ try:
120
+ print("feishu send_table resp:", resp.status_code, resp.json())
121
+ except Exception:
122
+ print("feishu send_table resp:", resp.status_code, resp.text)
123
+ return bool(resp.ok and resp.json().get("StatusCode") == 0)
124
+
125
+ async def send_table_async(
126
+ self,
127
+ headers: list[str],
128
+ rows: list[list[str]],
129
+ title: str,
130
+ template: str = "turquoise",
131
+ ):
132
+ return await asyncio.to_thread(self.send_table, headers, rows, title, template)
@@ -0,0 +1,143 @@
1
+ import requests
2
+ from typing import Generator
3
+
4
+
5
+ class Request:
6
+ def __init__(self, trust_env: bool = False):
7
+ self.session = requests.Session()
8
+ self.session.trust_env = trust_env
9
+
10
+ def _post(
11
+ self,
12
+ url: str,
13
+ data: dict = {},
14
+ headers: dict = {"Content-Type": "application/json"},
15
+ timeout: int = 0,
16
+ ):
17
+ kwargs = {
18
+ "url": url,
19
+ "json": data,
20
+ "headers": headers,
21
+ }
22
+ if timeout:
23
+ kwargs["timeout"] = timeout
24
+ response = self.session.post(**kwargs)
25
+ return response
26
+
27
+ def _get(self, url: str, params: dict = {}, headers: dict = {}, timeout: int = 0):
28
+ kwargs = {
29
+ "url": url,
30
+ "params": params,
31
+ "headers": headers,
32
+ }
33
+ if timeout:
34
+ kwargs["timeout"] = timeout
35
+ response = self.session.get(**kwargs)
36
+ return response
37
+
38
+ def post(
39
+ self,
40
+ url: str,
41
+ data: dict = {},
42
+ headers: dict = {"Content-Type": "application/json"},
43
+ timeout: int = 0,
44
+ ) -> tuple[dict, bool]:
45
+ """
46
+ description:
47
+ 发送 POST 请求
48
+ parameters:
49
+ url(str): 请求 URL
50
+ data(dict): 请求数据
51
+ headers(dict): 请求头
52
+ timeout(int): 请求超时时间
53
+ return:
54
+ response(dict): 响应数据
55
+ flag(bool): 请求是否成功
56
+ """
57
+ response = self._post(url, data, headers, timeout)
58
+
59
+ if response.status_code == 200:
60
+ return response.json(), True
61
+ else:
62
+ print(response.status_code, response.text)
63
+ return {
64
+ "status_code": response.status_code,
65
+ "message": response.text,
66
+ }, False
67
+
68
+ def get(
69
+ self,
70
+ url: str,
71
+ params: dict = {},
72
+ headers: dict = {},
73
+ timeout: int = 0,
74
+ ) -> tuple[dict, bool]:
75
+ """
76
+ description:
77
+ 发送 GET 请求
78
+ parameters:
79
+ url(str): 请求 URL
80
+ params(dict): 请求参数
81
+ headers(dict): 请求头
82
+ timeout(int): 请求超时时间
83
+ return:
84
+ response(dict): 响应数据
85
+ flag(bool): 请求是否成功
86
+ """
87
+ response = self._get(url, params, headers, timeout)
88
+
89
+ if response.status_code == 200:
90
+ return response.json(), True
91
+ else:
92
+ print(response.status_code, response.text)
93
+ return {
94
+ "status_code": response.status_code,
95
+ "message": response.text,
96
+ }, False
97
+
98
+ def post_stream(
99
+ self,
100
+ url: str,
101
+ data: dict = {},
102
+ headers: dict = {"Content-Type": "application/json"},
103
+ timeout: int = 0,
104
+ ) -> Generator[str, None, None]:
105
+ """
106
+ description:
107
+ 发送 POST 流式请求
108
+ parameters:
109
+ url(str): 请求 URL
110
+ data(dict): 请求数据
111
+ headers(dict): 请求头
112
+ timeout(int): 请求超时时间
113
+ return:
114
+ response(generator): 响应数据生成器
115
+ """
116
+ response = self._post(url, data, headers, timeout)
117
+ if response.status_code == 200:
118
+ return response.iter_lines()
119
+
120
+ def get_stream(
121
+ self,
122
+ url: str,
123
+ params: dict = {},
124
+ headers: dict = {},
125
+ timeout: int = 0,
126
+ ) -> Generator[str, None, None]:
127
+ """
128
+ description:
129
+ 发送 GET 流式请求
130
+ parameters:
131
+ url(str): 请求 URL
132
+ params(dict): 请求参数
133
+ headers(dict): 请求头
134
+ timeout(int): 请求超时时间
135
+ return:
136
+ response(generator): 响应数据生成器
137
+ """
138
+ response = self._get(url, params, headers, timeout)
139
+ if response.status_code == 200:
140
+ return response.iter_lines()
141
+
142
+ def __del__(self):
143
+ self.session.close()