ddclient 0.1.0__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,61 @@
1
+ Metadata-Version: 2.3
2
+ Name: ddclient
3
+ Version: 0.1.0
4
+ Summary: 钉钉开放平台服务端接口调用客户端
5
+ Author: luyifo
6
+ Author-email: luyifo <luyifo@163.com>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Dist: pydantic-settings>=2.12.0
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # 使用指导
14
+ ## Step 1
15
+ ```bash
16
+ pip install ddapi
17
+ ```
18
+
19
+ ## Step 2
20
+ 在项目根目录下新建.env文件
21
+ 配置环境变量
22
+ ```toml
23
+ DINGTALK_APP_KEY="xxxxxxxx"
24
+ DINGTALK_APP_SECRET="xxxxxxxx"
25
+ ```
26
+ 用你能想到的方法加载.env中的配置好的环境变量
27
+
28
+ ## Step 3
29
+
30
+ ```python
31
+ from ddapi import default_client as client
32
+ ```
33
+
34
+ ## 宜搭
35
+ 配置环境变量
36
+ ```toml
37
+ DINGTALK_APP_KEY="xxxxxxxx"
38
+ DINGTALK_APP_SECRET="xxxxxxxx"
39
+ DINGTALK_YIDA_APP_TYPE="xxxxxx"
40
+ DINGTALK_YIDA_SYSTEM_TOKEN="xxxxxx"
41
+ DINGTALK_USER_ID="xxxxxx"
42
+ ```
43
+
44
+ ### 默认使用
45
+ ```python
46
+ from ddapi import default_client as client
47
+
48
+ form_instance = client.yida.get_form_inst("FINST-XXXXXXX")
49
+ ```
50
+
51
+ ### 多应用
52
+ ```python
53
+ from ddapi import default_client as client,YiDa,YiDaConfig
54
+
55
+ yida1 = YiDa(client,YiDaConfig(app_type="app_type1",system_token="system_token1",user_id="user_id1"))
56
+ form_inst1 = yida1.get_form_inst("INST_ID")
57
+
58
+ yida2 = YiDa(client,YiDaConfig(app_type="app_type2",system_token="system_token2",user_id="user_id2"))
59
+ form_inst2 = yida2.get_form_inst("INST_ID")
60
+
61
+ ```
@@ -0,0 +1,49 @@
1
+ # 使用指导
2
+ ## Step 1
3
+ ```bash
4
+ pip install ddapi
5
+ ```
6
+
7
+ ## Step 2
8
+ 在项目根目录下新建.env文件
9
+ 配置环境变量
10
+ ```toml
11
+ DINGTALK_APP_KEY="xxxxxxxx"
12
+ DINGTALK_APP_SECRET="xxxxxxxx"
13
+ ```
14
+ 用你能想到的方法加载.env中的配置好的环境变量
15
+
16
+ ## Step 3
17
+
18
+ ```python
19
+ from ddapi import default_client as client
20
+ ```
21
+
22
+ ## 宜搭
23
+ 配置环境变量
24
+ ```toml
25
+ DINGTALK_APP_KEY="xxxxxxxx"
26
+ DINGTALK_APP_SECRET="xxxxxxxx"
27
+ DINGTALK_YIDA_APP_TYPE="xxxxxx"
28
+ DINGTALK_YIDA_SYSTEM_TOKEN="xxxxxx"
29
+ DINGTALK_USER_ID="xxxxxx"
30
+ ```
31
+
32
+ ### 默认使用
33
+ ```python
34
+ from ddapi import default_client as client
35
+
36
+ form_instance = client.yida.get_form_inst("FINST-XXXXXXX")
37
+ ```
38
+
39
+ ### 多应用
40
+ ```python
41
+ from ddapi import default_client as client,YiDa,YiDaConfig
42
+
43
+ yida1 = YiDa(client,YiDaConfig(app_type="app_type1",system_token="system_token1",user_id="user_id1"))
44
+ form_inst1 = yida1.get_form_inst("INST_ID")
45
+
46
+ yida2 = YiDa(client,YiDaConfig(app_type="app_type2",system_token="system_token2",user_id="user_id2"))
47
+ form_inst2 = yida2.get_form_inst("INST_ID")
48
+
49
+ ```
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "ddclient"
3
+ version = "0.1.0"
4
+ description = "钉钉开放平台服务端接口调用客户端"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "luyifo", email = "luyifo@163.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "httpx>=0.28.1",
12
+ "pydantic>=2.12.5",
13
+ "pydantic-settings>=2.12.0",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.9.18,<0.10.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=9.0.2",
23
+ ]
@@ -0,0 +1,9 @@
1
+ from .core import DingTalkClient, DingTalkConfig
2
+ from .yida import YiDa,YiDaConfig
3
+
4
+
5
+ _default_config = DingTalkConfig()
6
+
7
+ default_client = DingTalkClient(_default_config)
8
+
9
+ __all__ = ["DingTalkClient", "DingTalkConfig", "default_client","YiDa","YiDaConfig"]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from .core import DingTalkClient
7
+
8
+
9
+ class Contact:
10
+ def __init__(self, client: DingTalkClient) -> None:
11
+ self.client = client
12
+
13
+ def get_dept_list(self, dept_id: int = 1, lang: str = "zh_CN") -> dict[str, Any]:
14
+ """
15
+ 获取部门列表
16
+ https://open.dingtalk.com/document/development/user-management-acquires-the-list-departments
17
+
18
+ :param dept_id: 部门ID
19
+ :param lang: 语言 zh_CN en_US
20
+ :return {"errcode":0,"errmsg":"ok","result":[{"auto_add_user":true,"create_dept_group":true,"dept_id":37xxxx95,"name":"市场部","parent_id":1}],"request_id":"5um7ykyaalsj"}:
21
+ """
22
+ url = "https://oapi.dingtalk.com/topapi/v2/department/listsub"
23
+ return self.client._request(
24
+ url, "POST", json={"dept_id": dept_id, "language": lang}
25
+ )
26
+
27
+ def get_det_detail(self, dept_id: int, lang: str = "zh_CN") -> dict[str, Any]:
28
+ """
29
+ 获取部门详情
30
+ https://open.dingtalk.com/document/development/query-department-details0-v2
31
+
32
+ :param dept_id: 部门ID
33
+ :param lang: 语言 zh_CN en_US
34
+ :return {"errcode":0,"errmsg":"ok","result":{"dept_permits":[3,4,5],"outer_permit_users":["user123","1234"],"dept_manager_userid_list":["1020302901-431772414"],"org_dept_owner":"manager9153","outer_dept":false,"dept_group_chat_id":"chat1fccdb4b921f2bde18c26xxxx","group_contain_sub_dept":true,"auto_add_user":true,"hide_dept":false,"name":"测试","outer_permit_depts":[500,600],"user_permits":[],"dept_id":1,"create_dept_group":true,"order":0,"code":"100","union_dept_ext":{"corp_id":"test","dept_id":1234567}},"request_id":"4e7ljtq91rgo"}:
35
+ """
36
+ url = "https://oapi.dingtalk.com/topapi/v2/department/get"
37
+ return self.client._request(
38
+ url, "POST", json={"dept_id": dept_id, "language": lang}
39
+ )
40
+
41
+ def search_dept(self,query_word:str,offset:int = 0,size:int = 10):
42
+ """
43
+ 搜索部门ID
44
+ https://open.dingtalk.com/document/development/address-book-search-department-id
45
+
46
+ :param query_word: 部门名称或者部门名称拼音
47
+ :param offset: 分页页码
48
+ :param size: 分页大小
49
+ :return {"hasMore":false,"totalCount":2,"list":[220141953,220207936]}:
50
+ """
51
+
52
+ url = "https://api.dingtalk.com/v1.0/contact/departments/search"
53
+ return self.client._request(url,"POST",json={
54
+ "queryWord":query_word,
55
+ "offset":offset,
56
+ "size":size
57
+ })
58
+
59
+
60
+
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import httpx
5
+ import atexit
6
+ from typing_extensions import Self
7
+ from pydantic import BaseModel
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+ from typing import Literal, TYPE_CHECKING
10
+ from functools import cached_property
11
+
12
+ if TYPE_CHECKING:
13
+ from .yida import YiDa
14
+
15
+ HttpMethod = Literal["GET", "PUT", "POST", "DELETE"]
16
+
17
+
18
+ class DingTalkConfig(BaseSettings):
19
+ app_key: str = ""
20
+ app_secret: str = ""
21
+
22
+ model_config = SettingsConfigDict(
23
+ env_prefix="DINGTALK_",
24
+ env_file=".env",
25
+ env_file_encoding="utf-8",
26
+ extra="allow",
27
+ )
28
+
29
+
30
+ class Token(BaseModel):
31
+ access_token: str
32
+ expire_at: float
33
+
34
+
35
+ class TokenManager:
36
+ _instance = None
37
+ _tokens: dict[str, Token] = {}
38
+
39
+ def __new__(cls) -> Self:
40
+ if cls._instance is None:
41
+ cls._instance = super(TokenManager, cls).__new__(cls)
42
+
43
+ return cls._instance
44
+
45
+ def get_token(self, app_key: str, app_secret: str) -> str:
46
+ token = self._tokens.get(app_key)
47
+
48
+ if not token or token.expire_at < time.time():
49
+ token = self._refresh_token(app_key, app_secret)
50
+
51
+ return token.access_token
52
+
53
+ def _refresh_token(self, app_key: str, app_secret: str) -> Token:
54
+ url = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
55
+ body = {"appKey": app_key, "appSecret": app_secret}
56
+
57
+ with httpx.Client() as client:
58
+ res = client.post(url, json=body)
59
+
60
+ try:
61
+ res.raise_for_status()
62
+ except httpx.HTTPStatusError as ex:
63
+ raise Exception(ex.response.text)
64
+
65
+ data = res.json()
66
+
67
+ self._tokens[app_key] = Token(
68
+ access_token=data["accessToken"], expire_at=time.time() + 3600
69
+ )
70
+
71
+ return self._tokens[app_key]
72
+
73
+
74
+ class DingTalkClient:
75
+ def __init__(self, config: DingTalkConfig):
76
+ self.config = config
77
+ self.token_manager = TokenManager()
78
+ self.http_client = httpx.Client(
79
+ transport=httpx.HTTPTransport(retries=3), timeout=30.0
80
+ )
81
+
82
+ atexit.register(self.close)
83
+
84
+ @property
85
+ def acccess_token(self):
86
+ return self.token_manager.get_token(self.config.app_key, self.config.app_secret)
87
+
88
+ def _request(self, url: str, method: HttpMethod, **kwargs) -> dict:
89
+ if "oapi.dingtalk.com" in url:
90
+ params = kwargs.get("params", {})
91
+ params["access_token"] = self.acccess_token
92
+ kwargs["params"] = params
93
+ else:
94
+ headers = kwargs.get("headers", {})
95
+ headers.setdefault("x-acs-dingtalk-access-token", self.acccess_token)
96
+ kwargs["headers"] = headers
97
+
98
+ res = self.http_client.request(method, url, **kwargs)
99
+ try:
100
+ res.raise_for_status()
101
+ data = res.json()
102
+ except httpx.HTTPStatusError as ex:
103
+ try:
104
+ error_detail = ex.response.json()
105
+ except: # noqa: E722
106
+ error_detail = ex.response.text
107
+
108
+ raise Exception(
109
+ f"Http Error:{ex.response.status_code}, Detail:{error_detail}"
110
+ )
111
+
112
+ if "errcode" in data and data["errcode"] != 0:
113
+ raise Exception(
114
+ f"dingtalk api err: {data.get('errmsg', 'unknown')} code:{data.get['errcode']}"
115
+ )
116
+ return data
117
+
118
+ def close(self):
119
+ if hasattr(self, "http_client"):
120
+ self.http_client.close()
121
+
122
+ @cached_property
123
+ def yida(self) -> YiDa:
124
+ from .yida import YiDa, YiDaConfig
125
+
126
+ return YiDa(self, YiDaConfig())
File without changes
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Any
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+ import json
5
+
6
+ if TYPE_CHECKING:
7
+ from .core import DingTalkClient
8
+
9
+
10
+ class YiDaConfig(BaseSettings):
11
+ app_type: str | None = None
12
+ system_token: str | None = None
13
+ user_id: str | None = None
14
+
15
+ model_config = SettingsConfigDict(
16
+ env_file=".env",
17
+ env_prefix="DINGTALK_YIDA_",
18
+ env_file_encoding="utf-8",
19
+ extra="allow",
20
+ )
21
+
22
+
23
+ class YiDa:
24
+ def __init__(self, client: DingTalkClient, config: YiDaConfig) -> None:
25
+ self.client = client
26
+ self.config = config
27
+
28
+ @property
29
+ def _common_params(self):
30
+ return {
31
+ "appType": self.config.app_type,
32
+ "systemToken": self.config.system_token,
33
+ "userId": self.config.user_id,
34
+ }
35
+
36
+ def _prepare_params(
37
+ self, base_params: dict[str, Any], use_alias: bool, form_uuid: str | None
38
+ ):
39
+ params = {**base_params, "useAlias": use_alias}
40
+
41
+ if use_alias:
42
+ if form_uuid is None:
43
+ raise ValueError(
44
+ "when the 'use_alias' is true,the 'form_uuid' cannot be empty"
45
+ )
46
+
47
+ params["formUuid"] = form_uuid
48
+ return params
49
+
50
+ def get_form_inst(
51
+ self, inst_id: str, use_alias=False, form_uuid: str | None = None
52
+ ) -> dict[str, Any]:
53
+ """
54
+ 获取表单实例
55
+
56
+ :param inst_id: 表单实例ID
57
+ :param use_alias: 是否使用组件别名
58
+ :param form_uuid: 表单UUID 当使用组件别名时必填
59
+
60
+ :return {"originator":{"userId":"user123","name":{"nameInChinese":"张三","nameInEnglish":"ZhangSan","type":"i18n"},"departmentName":"开发部","email":"abc@alimail.com"},"modifiedTimeGMT":"2021-05-01","formInstId":"FORM_INST_12345"}:
61
+
62
+ """
63
+ url = f"https://api.dingtalk.com/v2.0/yida/forms/instances/{inst_id}"
64
+
65
+ params = self._prepare_params(self._common_params, use_alias, form_uuid)
66
+ return self.client._request(url, "GET", params=params)
67
+
68
+ def del_form_inst(self, inst_id: str):
69
+ url = "https://api.dingtalk.com/v1.0/yida/forms/instances"
70
+ return self.client._request(
71
+ url,
72
+ "DELETE",
73
+ params={**self._common_params, "formInstanceId": inst_id},
74
+ )
75
+
76
+ def save_form_inst(
77
+ self, form_uuid: str, form_data: dict[str, Any], use_alias: bool = False
78
+ ) -> dict[str, str]:
79
+ """
80
+ 新增表单实例
81
+
82
+ :param form_uuid: 表单UUID
83
+ :param form_data: 表单数据
84
+ :param use_alias: 是否使用组件别名
85
+ :return {"result":["FINST-SASNOO39NSIFF780"]}:
86
+
87
+ """
88
+ url = "https://api.dingtalk.com/v2.0/yida/forms/instances"
89
+ return self.client._request(
90
+ url,
91
+ "POST",
92
+ json={
93
+ **self._common_params,
94
+ "formUuid": form_uuid,
95
+ "formDataJson": json.dumps(form_data),
96
+ "useAlias": use_alias,
97
+ },
98
+ )
99
+
100
+ def update_form_inst(
101
+ self,
102
+ inst_id: str,
103
+ form_data: dict[str, Any],
104
+ use_alias: bool = False,
105
+ form_uuid: str | None = None,
106
+ use_latest_version=False,
107
+ ):
108
+ """
109
+ 更新表单实例
110
+ https://open.dingtalk.com/document/development/api-updateformdata-v2
111
+
112
+ :param inst_id: 表单实例ID
113
+ :param form_data: 表单数据
114
+ :param use_alias: 是否使用组件别名
115
+ :param form_uuid: 表单UUID 当使用组件别名时必填
116
+ :param use_latest_version: 是否使用最新表单版本更新
117
+ """
118
+
119
+ url = "https://api.dingtalk.com/v2.0/yida/forms/instances"
120
+ body = {
121
+ **self._common_params,
122
+ "formInstanceId": inst_id,
123
+ "updateFormDataJson": json.dumps(form_data),
124
+ "useAlias": use_alias,
125
+ "useLatestVersion": use_latest_version,
126
+ }
127
+
128
+ body = self._prepare_params(
129
+ {
130
+ **self._common_params,
131
+ "formInstanceId": inst_id,
132
+ "updateFormDataJson": json.dumps(form_data),
133
+ "useLatestVersion": use_latest_version,
134
+ },
135
+ use_alias,
136
+ form_uuid,
137
+ )
138
+
139
+ self.client._request(url, "PUT", json=body)
140
+
141
+ def save_update_form_inst(
142
+ self,
143
+ form_uuid: str,
144
+ search_condition: dict[str, Any],
145
+ form_data: dict[str, Any],
146
+ use_alias: bool = False,
147
+ execute_expression: bool = True,
148
+ ):
149
+ """
150
+ 新增或更新表单实例
151
+ https://open.dingtalk.com/document/development/api-createorupdateformdata-v2
152
+
153
+ :param form_uuid: 表单UUID
154
+ :param search_condition: 检索条件
155
+ :param form_data: 表单数据
156
+ :param use_alias: 是否使用组件别名
157
+ :param execute_expression: 是否触发表单校验规则,关联业务规则和第三方服务回调
158
+
159
+ :return {"result":["FINST-SASNOO39NSIFF780"]}:
160
+ """
161
+
162
+ url = "https://api.dingtalk.com/v2.0/yida/forms/instances/insertOrUpdate"
163
+
164
+ return self.client._request(
165
+ url,
166
+ "POST",
167
+ json={
168
+ **self._common_params,
169
+ "formUuid": form_uuid,
170
+ "searchCondition": search_condition,
171
+ "formDataJson": json.dumps(form_data),
172
+ "noExecuteExpression": not execute_expression,
173
+ "useAlias": use_alias,
174
+ },
175
+ )