├── .bumpversion.cfg
├── .gitignore
├── .readthedocs.yml
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── dev-requirements.txt
├── dingtalk
├── __init__.py
├── client
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── attendance.py
│ │ ├── base.py
│ │ ├── blackboard.py
│ │ ├── bpms.py
│ │ ├── calendar.py
│ │ ├── callback.py
│ │ ├── chat.py
│ │ ├── checkin.py
│ │ ├── cspace.py
│ │ ├── department.py
│ │ ├── employeerm.py
│ │ ├── ext.py
│ │ ├── extcontact.py
│ │ ├── health.py
│ │ ├── message.py
│ │ ├── microapp.py
│ │ ├── report.py
│ │ ├── role.py
│ │ ├── taobao.py
│ │ ├── user.py
│ │ └── workrecord.py
│ ├── base.py
│ ├── channel.py
│ └── isv.py
├── core
│ ├── __init__.py
│ ├── constants.py
│ ├── exceptions.py
│ └── utils.py
├── crypto
│ ├── __init__.py
│ ├── base.py
│ ├── cryptography.py
│ ├── pkcs7.py
│ └── pycrypto.py
├── model
│ ├── __init__.py
│ ├── field.py
│ └── message.py
└── storage
│ ├── __init__.py
│ ├── cache.py
│ ├── kvstorage.py
│ └── memorystorage.py
├── docs
├── Makefile
├── changelog.rst
├── client
│ ├── api
│ │ ├── attendance.rst
│ │ ├── blackboard.rst
│ │ ├── bpms.rst
│ │ ├── calendar.rst
│ │ ├── callback.rst
│ │ ├── chat.rst
│ │ ├── checkin.rst
│ │ ├── cspace.rst
│ │ ├── department.rst
│ │ ├── employeerm.rst
│ │ ├── ext.rst
│ │ ├── extcontact.rst
│ │ ├── health.rst
│ │ ├── message.rst
│ │ ├── microapp.rst
│ │ ├── report.rst
│ │ ├── role.rst
│ │ ├── taobao.rst
│ │ ├── user.rst
│ │ └── workrecord.rst
│ ├── index.rst
│ └── isv.rst
├── conf.py
├── index.rst
├── install.rst
└── model
│ ├── field.rst
│ └── message.rst
├── pytest.ini
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
├── test_crypto.py
├── test_message.py
├── test_storage.py
└── test_utils.py
└── tox.ini
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | files = setup.py dingtalk/__init__.py
3 | commit = True
4 | tag = True
5 | current_version = 1.3.8
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 | .pytest_cache
91 |
92 | .idea/
93 |
94 | .DS_Store
95 |
96 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | formats:
2 | - none
3 | python:
4 | version: 3
5 | pip_install: true
6 | extra_requirements:
7 | - cryptography
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | # Use container-based infrastructure
4 | sudo: false
5 |
6 | matrix:
7 | include:
8 | - env: TOX_ENV=py27-cryptography
9 | python: 2.7
10 | - env: TOX_ENV=py27-pycrypto
11 | python: 2.7
12 | - env: TOX_ENV=py34-cryptography
13 | python: 3.4
14 | - env: TOX_ENV=py34-pycrypto
15 | python: 3.4
16 | - env: TOX_ENV=py35-cryptography
17 | python: 3.5
18 | - env: TOX_ENV=py35-pycrypto
19 | python: 3.5
20 | - env: TOX_ENV=py36-cryptography
21 | python: 3.6
22 | - env: TOX_ENV=py36-pycrypto
23 | python: 3.6
24 | - env: TOX_ENV=pypy-cryptography
25 | python: "pypy"
26 | - env: TOX_ENV=pypy3-cryptography
27 | python: "pypy3"
28 |
29 | services:
30 | - redis-server
31 | - memcached
32 |
33 | cache:
34 | directories:
35 | - $HOME/.cache/pip
36 |
37 | install:
38 | - pip install tox
39 | - pip install "flake8>=3.7"
40 |
41 | before_script:
42 | - "flake8 ."
43 |
44 | script:
45 | tox -e $TOX_ENV
46 |
47 | after_success:
48 | - |
49 | if [[ "${TRAVIS_TAG:-}" != "" && "$TOX_ENV" == "py36-cryptography" ]]; then
50 | python3.6 setup.py sdist bdist_wheel;
51 | python3.6 -m pip install twine;
52 | python3.6 -m twine upload --skip-existing dist/*;
53 | fi
54 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include requirements.txt
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | #######################
2 | DingTalk Sdk for Python
3 | #######################
4 | .. image:: https://travis-ci.org/007gzs/dingtalk-sdk.svg?branch=master
5 | :target: https://travis-ci.org/007gzs/dingtalk-sdk
6 | .. image:: https://img.shields.io/pypi/v/dingtalk-sdk.svg
7 | :target: https://pypi.org/project/dingtalk-sdk
8 |
9 | 钉钉开放平台第三方 Python SDK。
10 | `【阅读文档】 `_。
11 |
12 | ********
13 | 功能特性
14 | ********
15 | + 企业内部开发接入api
16 | + 应用服务商(ISV)接入api
17 |
18 | ********
19 | 安装
20 | ********
21 |
22 | 目前 dingtalk-sdk 支持的 Python 环境有 2.7, 3.4, 3.5, 3.6 和 pypy。
23 |
24 | dingtalk-sdk 消息加解密同时兼容 cryptography 和 PyCrypto, 优先使用 cryptography 库。
25 | 可先自行安装 cryptography 或者 PyCrypto 库::
26 |
27 | # 安装 cryptography
28 | pip install cryptography>=0.8.2
29 | # 或者安装 PyCrypto
30 | pip install pycrypto>=2.6.1
31 |
32 | 为了简化安装过程,推荐使用 pip 进行安装
33 |
34 | .. code-block:: bash
35 |
36 | pip install dingtalk-sdk
37 | # with cryptography
38 | pip install dingtalk-sdk[cryptography]
39 | # with pycrypto
40 | pip install dingtalk-sdk[pycrypto]
41 |
42 | 升级 dingtalk-sdk 到新版本::
43 |
44 | pip install -U dingtalk-sdk
45 |
46 | ****************
47 | 使用示例
48 | ****************
49 |
50 | django 示例 https://github.com/007gzs/dingtalk-django-example
51 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | pytest
3 | redis
4 | pymemcache
5 |
6 |
--------------------------------------------------------------------------------
/dingtalk/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, unicode_literals
2 |
3 | import logging
4 |
5 | from dingtalk.client import SecretClient, AppKeyClient # NOQA
6 | from dingtalk.client.isv import ISVClient # NOQA
7 | from dingtalk.core.exceptions import DingTalkClientException, DingTalkException # NOQA
8 |
9 | __version__ = '1.3.8'
10 | __author__ = '007gzs'
11 |
12 | # Set default logging handler to avoid "No handler found" warnings.
13 | try: # Python 2.7+
14 | from logging import NullHandler
15 | except ImportError:
16 | class NullHandler(logging.Handler):
17 | def emit(self, record):
18 | pass
19 |
20 | logging.getLogger(__name__).addHandler(NullHandler())
21 |
--------------------------------------------------------------------------------
/dingtalk/client/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import logging
5 |
6 | import time
7 |
8 | from dingtalk.client import api
9 | from dingtalk.client.api.taobao import TaobaoMixin
10 | from dingtalk.client.base import BaseClient
11 | from dingtalk.core.utils import DingTalkSigner, random_string
12 | from dingtalk.crypto import DingTalkCrypto
13 | from dingtalk.storage.cache import DingTalkCache
14 |
15 | logger = logging.getLogger(__name__)
16 |
17 |
18 | class DingTalkClient(BaseClient, TaobaoMixin):
19 |
20 | attendance = api.Attendance()
21 | blackboard = api.BlackBoard()
22 | bpms = api.Bpms()
23 | calendar = api.Calendar()
24 | callback = api.Callback()
25 | chat = api.Chat()
26 | checkin = api.Checkin()
27 | cspace = api.Cspace()
28 | department = api.Department()
29 | ext = api.Ext()
30 | extcontact = api.ExtContact()
31 | employeerm = api.Employeerm()
32 | health = api.Health()
33 | message = api.Message()
34 | microapp = api.MicroApp()
35 | report = api.Report()
36 | role = api.Role()
37 | user = api.User()
38 | workrecord = api.WorkRecord()
39 |
40 | def __init__(self, corp_id, prefix='client', storage=None, timeout=None, auto_retry=True):
41 | super(DingTalkClient, self).__init__(storage, timeout, auto_retry)
42 | self.corp_id = corp_id
43 | self.cache = DingTalkCache(self.storage, "%s:%s" % (prefix, self.get_access_token_key()))
44 |
45 | def get_access_token_key(self):
46 | return "corp_id:%s" % self.corp_id
47 |
48 | @property
49 | def access_token(self):
50 | token = self.cache.access_token.get()
51 | if token is None:
52 | ret = self.get_access_token()
53 | token = ret['access_token']
54 | expires_in = ret.get('expires_in', 7200)
55 | self.cache.access_token.set(value=token, ttl=expires_in)
56 | return token
57 |
58 | @property
59 | def jsapi_ticket(self):
60 | ticket = self.cache.jsapi_ticket.get()
61 | if ticket is None:
62 | ret = self.get_jsapi_ticket()
63 | ticket = ret['ticket']
64 | expires_in = ret.get('expires_in', 7200)
65 | self.cache.jsapi_ticket.set(value=ticket, ttl=expires_in)
66 | return ticket
67 |
68 | def get_jsapi_params(self, url, noncestr=None, timestamp=None):
69 | if not noncestr:
70 | noncestr = random_string()
71 | if timestamp is None:
72 | timestamp = int(time.time() * 1000)
73 | data = [
74 | 'noncestr={noncestr}'.format(noncestr=noncestr),
75 | 'jsapi_ticket={ticket}'.format(ticket=self.jsapi_ticket),
76 | 'timestamp={timestamp}'.format(timestamp=timestamp),
77 | 'url={url}'.format(url=url),
78 | ]
79 | signer = DingTalkSigner(delimiter=b'&')
80 | signer.add_data(*data)
81 |
82 | ret = {
83 | 'corpId': self.corp_id,
84 | 'timeStamp': timestamp,
85 | 'nonceStr': noncestr,
86 | 'signature': signer.signature
87 | }
88 | return ret
89 |
90 | def _handle_pre_request(self, method, uri, kwargs):
91 | if 'access_token=' in uri or 'access_token' in kwargs.get('params', {}):
92 | raise ValueError("uri参数中不允许有access_token: " + uri)
93 | uri = '%s%saccess_token=%s' % (uri, '&' if '?' in uri else '?', self.access_token)
94 | return method, uri, kwargs
95 |
96 | def _handle_pre_top_request(self, params, uri):
97 | if 'session=' in uri or 'session' in params:
98 | raise ValueError("uri参数中不允许有session: " + uri)
99 | params['session'] = self.access_token
100 |
101 | return super(DingTalkClient, self)._handle_pre_top_request(params, uri)
102 |
103 | def _handle_request_except(self, e, func, *args, **kwargs):
104 | if e.errcode in (33001, 40001, 42001, 40014):
105 | self.cache.access_token.delete()
106 | if self.auto_retry:
107 | return func(*args, **kwargs)
108 | raise e
109 |
110 | def get_jsapi_ticket(self):
111 | return self.get('/get_jsapi_ticket')
112 |
113 | def get_access_token(self):
114 | raise NotImplementedError
115 |
116 |
117 | class SecretClient(DingTalkClient):
118 |
119 | def __init__(self, corp_id, corp_secret, token=None, aes_key=None, storage=None, timeout=None, auto_retry=True):
120 | super(SecretClient, self).__init__(corp_id, 'secret:'+corp_id, storage, timeout, auto_retry)
121 | self.corp_secret = corp_secret
122 | self.crypto = DingTalkCrypto(token, aes_key, corp_id)
123 |
124 | def get_access_token(self):
125 | return self._request(
126 | 'GET',
127 | '/gettoken',
128 | params={'corpid': self.corp_id, 'corpsecret': self.corp_secret}
129 | )
130 |
131 |
132 | class AppKeyClient(DingTalkClient):
133 |
134 | def __init__(self, corp_id, app_key, app_secret, token=None, aes_key=None, storage=None, timeout=None,
135 | auto_retry=True):
136 | self.app_key = app_key
137 | self.app_secret = app_secret
138 | super(AppKeyClient, self).__init__(corp_id, 'secret:' + corp_id, storage, timeout, auto_retry)
139 | self.crypto = DingTalkCrypto(token, aes_key, corp_id)
140 |
141 | def get_access_token_key(self):
142 | return "app_key:%s" % self.app_key
143 |
144 | def get_access_token(self):
145 | return self._request(
146 | 'GET',
147 | '/gettoken',
148 | params={'appkey': self.app_key, 'appsecret': self.app_secret}
149 | )
150 |
--------------------------------------------------------------------------------
/dingtalk/client/api/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.attendance import Attendance # NOQA
5 | from dingtalk.client.api.blackboard import BlackBoard # NOQA
6 | from dingtalk.client.api.bpms import Bpms # NOQA
7 | from dingtalk.client.api.calendar import Calendar # NOQA
8 | from dingtalk.client.api.callback import Callback # NOQA
9 | from dingtalk.client.api.chat import Chat # NOQA
10 | from dingtalk.client.api.checkin import Checkin # NOQA
11 | from dingtalk.client.api.cspace import Cspace # NOQA
12 | from dingtalk.client.api.department import Department # NOQA
13 | from dingtalk.client.api.ext import Ext # NOQA
14 | from dingtalk.client.api.extcontact import ExtContact # NOQA
15 | from dingtalk.client.api.employeerm import Employeerm # NOQA
16 | from dingtalk.client.api.health import Health # NOQA
17 | from dingtalk.client.api.message import Message # NOQA
18 | from dingtalk.client.api.microapp import MicroApp # NOQA
19 | from dingtalk.client.api.report import Report # NOQA
20 | from dingtalk.client.api.role import Role # NOQA
21 | from dingtalk.client.api.user import User # NOQA
22 | from dingtalk.client.api.workrecord import WorkRecord # NOQA
23 |
--------------------------------------------------------------------------------
/dingtalk/client/api/attendance.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import time
5 | import datetime
6 |
7 | from dingtalk.client.api.base import DingTalkBaseAPI
8 |
9 |
10 | class Attendance(DingTalkBaseAPI):
11 |
12 | DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
13 |
14 | def list_record(self, user_ids, check_date_from, check_date_to):
15 | """
16 | 考勤打卡记录开放
17 |
18 | :param user_ids: 企业内的员工id列表,最多不能超过50个
19 | :param check_date_from: 查询考勤打卡记录的起始工作日
20 | :param check_date_to: 查询考勤打卡记录的结束工作日。注意,起始与结束工作日最多相隔7天
21 | :return:
22 | """
23 | if isinstance(check_date_from, (datetime.date, datetime.datetime)):
24 | check_date_from = check_date_from.strftime(self.DATE_TIME_FORMAT)
25 | if isinstance(check_date_to, (datetime.date, datetime.datetime)):
26 | check_date_to = check_date_to.strftime(self.DATE_TIME_FORMAT)
27 |
28 | return self._post(
29 | '/attendance/listRecord',
30 | {
31 | "userIds": user_ids,
32 | "checkDateFrom": check_date_from,
33 | "checkDateTo": check_date_to
34 | },
35 | result_processor=lambda x: x['recordresult']
36 | )
37 |
38 | def list(self, work_date_from, work_date_to, user_ids=(), offset=0, limit=50):
39 | """
40 | 考勤打卡数据开放
41 |
42 | :param work_date_from: 查询考勤打卡记录的起始工作日
43 | :param work_date_to: 查询考勤打卡记录的结束工作日
44 | :param user_ids: 员工在企业内的UserID列表,企业用来唯一标识用户的字段
45 | :param offset: 表示获取考勤数据的起始点,第一次传0,如果还有多余数据,下次获取传的offset值为之前的offset+limit
46 | :param limit: 表示获取考勤数据的条数,最大不能超过50条
47 | :return:
48 | """
49 | if isinstance(work_date_from, (datetime.date, datetime.datetime)):
50 | work_date_from = work_date_from.strftime(self.DATE_TIME_FORMAT)
51 | if isinstance(work_date_to, (datetime.date, datetime.datetime)):
52 | work_date_to = work_date_to.strftime(self.DATE_TIME_FORMAT)
53 |
54 | return self._post(
55 | '/attendance/list',
56 | {
57 | "workDateFrom": work_date_from,
58 | "workDateTo": work_date_to,
59 | "userIdList": user_ids,
60 | "offset": offset,
61 | "limit": limit
62 | }
63 | )
64 |
65 | def listschedule(self, work_date, offset=0, size=200):
66 | """
67 | 考勤排班信息按天全量查询接
68 |
69 | :param work_date: 排班时间
70 | :param offset: 偏移位置
71 | :param size: 分页大小,最大200
72 | :return:
73 | """
74 | if isinstance(work_date, (datetime.date, datetime.datetime)):
75 | work_date = work_date.strftime(self.DATE_TIME_FORMAT)
76 | return self._top_request(
77 | 'dingtalk.smartwork.attends.listschedule',
78 | {
79 | "work_date": work_date,
80 | "offset": offset,
81 | "size": size
82 | }
83 | )
84 |
85 | def getsimplegroups(self, offset=0, size=10):
86 | """
87 | 获取考勤组列表详情
88 |
89 | :param offset: 偏移位置
90 | :param size: 分页大小,最大10
91 | :return:
92 | """
93 | return self._top_request(
94 | 'dingtalk.smartwork.attends.getsimplegroups',
95 | {
96 | "offset": offset,
97 | "size": size
98 | }
99 | )
100 |
101 | def getleaveapproveduration(self, userid, from_date, to_date):
102 | """
103 | 计算请假时长
104 |
105 | :param userid: 员工在企业内的UserID,企业用来唯一标识用户的字段。
106 | :param from_date: 请假开始时间
107 | :param to_date: 请假结束时间
108 | :return: 请假时长(单位分钟)
109 | """
110 | if isinstance(from_date, (datetime.date, datetime.datetime)):
111 | from_date = from_date.strftime(self.DATE_TIME_FORMAT)
112 | if isinstance(to_date, (datetime.date, datetime.datetime)):
113 | to_date = to_date.strftime(self.DATE_TIME_FORMAT)
114 |
115 | return self._top_request(
116 | 'dingtalk.smartwork.attends.getleaveapproveduration',
117 | {
118 | "userid": userid,
119 | "from_date": from_date,
120 | "to_date": to_date
121 | },
122 | result_processor=lambda x: x['duration_in_minutes']
123 | )
124 |
125 | def getleavestatus(self, userid_list, start_time, end_time, offset=0, size=20):
126 | """
127 | 请假状态查询接口
128 | 该接口用于查询指定企业下的指定用户在指定时间段内的请假状态
129 |
130 | :param userid_list: 待查询用户id列表,支持最多100个用户的批量查询
131 | :param start_time: 开始时间 ,时间戳,支持最多180天的查询
132 | :param end_time: 结束时间,时间戳,支持最多180天的查询
133 | :param offset: 分页偏移,非负整数
134 | :param size: 分页大小,正整数,最大20
135 | """
136 | if isinstance(start_time, (datetime.date, datetime.datetime)):
137 | start_time = int(time.mktime(start_time.timetuple()) * 1000)
138 | if isinstance(end_time, (datetime.date, datetime.datetime)):
139 | end_time = int(time.mktime(end_time.timetuple()) * 1000)
140 | return self._top_request(
141 | "dingtalk.oapi.attendance.getleavestatus",
142 | {
143 | "userid_list": userid_list,
144 | "start_time": start_time,
145 | "end_time": end_time,
146 | "offset": offset,
147 | "size": size
148 | }
149 | )
150 |
151 | def getusergroup(self, userid):
152 | """
153 | 获取用户考勤组
154 |
155 | :param userid: 员工在企业内的UserID,企业用来唯一标识用户的字段。
156 | :return:
157 | """
158 | return self._top_request(
159 | 'dingtalk.smartwork.attends.getusergroup',
160 | {"userid": userid}
161 | )
162 |
--------------------------------------------------------------------------------
/dingtalk/client/api/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 |
5 | class DingTalkBaseAPI(object):
6 |
7 | API_BASE_URL = None
8 |
9 | def __init__(self, client=None):
10 | self._client = client
11 |
12 | def _get(self, url, params=None, **kwargs):
13 | if self.API_BASE_URL:
14 | kwargs['api_base_url'] = self.API_BASE_URL
15 | return self._client.get(url, params, **kwargs)
16 |
17 | def _post(self, url, data=None, params=None, **kwargs):
18 | if self.API_BASE_URL:
19 | kwargs['api_base_url'] = self.API_BASE_URL
20 | return self._client.post(url, data, params, **kwargs)
21 |
22 | def _top_request(self, method, params=None, format_='json', v='2.0',
23 | simplify='false', partner_id=None, url=None, **kwargs):
24 | if self.API_BASE_URL:
25 | kwargs['api_base_url'] = self.API_BASE_URL
26 | return self._client.top_request(method, params, format_, v, simplify, partner_id, url, **kwargs)
27 |
28 | @property
29 | def corp_id(self):
30 | return self._client.corp_id
31 |
--------------------------------------------------------------------------------
/dingtalk/client/api/blackboard.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 |
6 |
7 | class BlackBoard(DingTalkBaseAPI):
8 |
9 | def listtopten(self, userid):
10 | """
11 | 列出用户的公告列表
12 |
13 | :param userid: 用户id
14 | """
15 | return self._top_request(
16 | "dingtalk.oapi.blackboard.listtopten",
17 | {"userid": userid}
18 | )
19 |
--------------------------------------------------------------------------------
/dingtalk/client/api/bpms.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import datetime
5 | import time
6 |
7 | import six
8 | from dingtalk.client.api.base import DingTalkBaseAPI
9 | from dingtalk.core.utils import to_text
10 | from optionaldict import optionaldict
11 |
12 |
13 | class Bpms(DingTalkBaseAPI):
14 |
15 | def process_copy(self, agent_id, process_code, biz_category_id=None, process_name=None, description=None):
16 | """
17 | 复制审批流
18 |
19 | :param agent_id: 企业微应用标识
20 | :param process_code: 审批流的唯一码
21 | :param biz_category_id: 业务分类标识
22 | :param process_name: 审批流名称
23 | :param description: 审批流描述
24 | :return:
25 | """
26 |
27 | return self._top_request(
28 | 'dingtalk.smartwork.bpms.process.copy',
29 | optionaldict({
30 | 'agent_id': agent_id,
31 | 'process_code': process_code,
32 | 'biz_category_id': biz_category_id,
33 | 'process_name': process_name,
34 | 'description': description
35 | })
36 | )
37 |
38 | def process_sync(self, agent_id, src_process_code, target_process_code, biz_category_id=None, process_name=None):
39 | """
40 | 更新审批流
41 |
42 | :param agent_id: 企业微应用标识
43 | :param src_process_code: 源审批流的唯一码
44 | :param target_process_code: 目标审批流的唯一码
45 | :param biz_category_id: 业务分类标识
46 | :param process_name: 审批流名称
47 | :return:
48 | """
49 |
50 | return self._top_request(
51 | 'dingtalk.smartwork.bpms.process.sync',
52 | optionaldict({
53 | 'agent_id': agent_id,
54 | 'src_process_code': src_process_code,
55 | 'target_process_code': target_process_code,
56 | 'biz_category_id': biz_category_id,
57 | 'process_name': process_name
58 | })
59 | )
60 |
61 | def processinstance_create(
62 | self, process_code, originator_user_id, dept_id, approvers=None, form_component_values=None,
63 | agent_id=None, cc_list=(), cc_start=False, cc_finish=False, approvers_v2=None
64 | ):
65 | """
66 | 发起审批实例
67 |
68 | :param process_code: 审批流的唯一码
69 | :param originator_user_id: 审批实例发起人的userid
70 | :param dept_id: 发起人所在的部门
71 | :param approvers: 审批人userid列表
72 | :param form_component_values: 审批流表单参数 name: 表单每一栏的名称 value: 表单每一栏的值, ext_value: 扩展值
73 | 例:OrderedDict({name1: value1, name2: (value2, ext_value2), name3: (value3, )})
74 | :param agent_id:
75 | :param cc_list: 抄送人userid列表
76 | :param cc_start: 开始时抄送
77 | :param cc_finish: 结束时抄送
78 | :param approvers_v2: 审批人列表,支持会签/或签,优先级高于approvers变量
79 | :return:
80 | """
81 | cc_position = 'START' if cc_start else ''
82 | if cc_finish:
83 | if cc_position:
84 | cc_position += '_'
85 | cc_position += 'FINISH'
86 | if isinstance(approvers, (list, tuple, set)):
87 | approvers = ','.join(map(to_text, approvers))
88 | form_component_value_list = []
89 | if form_component_values:
90 | for name, value in form_component_values.items():
91 | data = {'name': name}
92 | if isinstance(value, (list, tuple)):
93 | if len(value) > 1:
94 | data['ext_value'] = value[1]
95 | value = value[0]
96 | data['value'] = value
97 | form_component_value_list.append(data)
98 |
99 | return self._top_request(
100 | "dingtalk.oapi.processinstance.create",
101 | optionaldict({
102 | "process_code": process_code,
103 | "originator_user_id": originator_user_id,
104 | "dept_id": dept_id,
105 | "form_component_values": form_component_value_list,
106 | "agent_id": agent_id,
107 | "approvers": approvers,
108 | "cc_list": cc_list,
109 | "cc_position": cc_position,
110 | "approvers_v2": approvers_v2
111 | }),
112 | result_processor=lambda x: x['process_instance_id']
113 | )
114 |
115 | def processinstance_listids(self, process_code, start_time, end_time, size='10', cursor='0', userid_list=()):
116 | """
117 | 分页获取审批实例id列表
118 | 企业可以根据审批流的唯一标识,分页获取该审批流对应的审批实例id。只能取到权限范围内的相关部门的审批实例
119 |
120 | :param process_code: 流程模板唯一标识,可在oa后台编辑审批表单部分查询
121 | :param start_time: 审批实例开始时间,毫秒级
122 | :param end_time: 审批实例结束时间,毫秒级,默认取当前值
123 | :param size: 分页参数,每页大小,最多传10
124 | :param cursor: 分页查询的游标,最开始传0,后续传返回参数中的next_cursor值
125 | :param userid_list: 发起人用户id列表
126 | """
127 | if isinstance(start_time, (datetime.date, datetime.datetime)):
128 | start_time = int(time.mktime(start_time.timetuple()) * 1000)
129 | if isinstance(end_time, (datetime.date, datetime.datetime)):
130 | end_time = int(time.mktime(end_time.timetuple()) * 1000)
131 | if isinstance(userid_list, (list, tuple)):
132 | userid_list = ','.join(map(to_text, userid_list))
133 | return self._top_request(
134 | "dingtalk.oapi.processinstance.listids",
135 | optionaldict({
136 | "process_code": process_code,
137 | "start_time": start_time,
138 | "end_time": end_time,
139 | "size": size,
140 | "cursor": cursor,
141 | "userid_list": userid_list
142 | })
143 | )
144 |
145 | def processinstance_list(self, process_code, start_time, end_time=None, cursor=0, size=10, userid_list=()):
146 | """
147 | 获取审批实例列表
148 |
149 | :param process_code: 流程模板唯一标识,可在oa后台编辑审批表单部分查询
150 | :param start_time: 审批实例开始时间
151 | :param end_time: 审批实例结束时间,默认取当前值
152 | :param cursor: 每页大小,最多传10
153 | :param size: 分页查询的游标,最开始传0,后续传返回参数中的next_cursor值
154 | :param userid_list: 发起人用户id列表
155 | :return:
156 | """
157 | userid_list = ','.join(map(to_text, userid_list))
158 |
159 | if isinstance(start_time, (datetime.date, datetime.datetime)):
160 | start_time = int(time.mktime(start_time.timetuple()) * 1000)
161 |
162 | if isinstance(end_time, (datetime.date, datetime.datetime)):
163 | end_time = int(time.mktime(end_time.timetuple()) * 1000)
164 |
165 | assert isinstance(start_time, six.integer_types)
166 | assert end_time is None or isinstance(end_time, six.integer_types)
167 |
168 | return self._top_request(
169 | 'dingtalk.smartwork.bpms.processinstance.list',
170 | optionaldict({
171 | 'process_code': process_code,
172 | 'start_time': start_time,
173 | 'end_time': end_time,
174 | 'cursor': cursor,
175 | 'size': size,
176 | 'userid_list': userid_list
177 | })
178 | )
179 |
180 | def processinstance_get(self, process_instance_id):
181 | """
182 | 获取单个审批实例详情
183 |
184 | :param process_instance_id: 审批实例id
185 | :return:
186 | """
187 | return self._top_request(
188 | 'dingtalk.smartwork.bpms.processinstance.get',
189 | {'process_instance_id': process_instance_id},
190 | result_processor=lambda x: x['process_instance']
191 | )
192 |
193 | def dingtalk_oapi_process_gettodonum(self, userid):
194 | """
195 | 获取待我审批数量
196 | 获取用户待审批数量
197 |
198 | :param userid: 用户id
199 | """
200 | return self._top_request(
201 | "dingtalk.oapi.process.gettodonum",
202 | {"userid": userid}
203 | )
204 |
205 | def process_listbyuserid(self, userid, offset=0, size=100):
206 | """
207 | 根据用户id获取可见审批模板列表
208 |
209 | :param userid: 用户id
210 | :param offset: 分页游标,从0开始。根据返回结果中next_cursor是否为空判断是否有下一页,且再次调用offset设置成next_cursor的值
211 | :param size: 分页大小,最大可设置成100
212 | :return:
213 | """
214 | return self._top_request(
215 | 'dingtalk.oapi.process.listbyuserid',
216 | {
217 | 'userid': userid,
218 | 'offset': offset,
219 | 'size': size
220 | }
221 | )
222 |
223 | def process_instance_terminate(self, process_instance_id, remark="", is_system=True, operating_userid=""):
224 | """
225 | 调用本接口通过实例id终止当前企业下发起的审批实例。
226 | 终止审批实例后,审批状态为“已撤销”。
227 | :param process_instance_id: 审批实例ID
228 | :param remark: 终止说明
229 | :param is_system: 是否通过系统操作:true:由系统直接终止false:由指定的操作者终止
230 | :param operating_userid: 操作人的userid, 当is_system为false时,该参数必传
231 | :return:
232 | """
233 | return self._top_request(
234 | "dingtalk.oapi.process.instance.terminate",
235 | {
236 | "request": {
237 | "process_instance_id": process_instance_id,
238 | "remark": remark,
239 | "is_system": is_system,
240 | "operating_userid": operating_userid
241 | }
242 | }
243 |
244 | )
245 |
--------------------------------------------------------------------------------
/dingtalk/client/api/calendar.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | from __future__ import absolute_import, unicode_literals
3 |
4 |
5 | from dingtalk.client.api.base import DingTalkBaseAPI
6 |
7 |
8 | class Calendar(DingTalkBaseAPI):
9 |
10 | def create(self, create_vo):
11 | """
12 | 创建日程
13 |
14 | :param create_vo: 创建日程实体
15 | """
16 | return self._top_request(
17 | "dingtalk.oapi.calendar.create",
18 | {"create_vo": create_vo}
19 | )
20 |
21 | def list(
22 | self,
23 | user_id,
24 | calendar_folder_id='',
25 | time_min=None,
26 | i_cal_uid='',
27 | single_events='',
28 | page_token='',
29 | max_results=250,
30 | time_max=None
31 | ):
32 | """
33 | 日程查询
34 |
35 | :param user_id: 员工ID
36 | :param calendar_folder_id: 钉钉日历文件夹的对外id,默认是自己的默认文件夹
37 | :param time_min: 查询时间下限
38 | :param i_cal_uid: 日程跨域唯一id,用于唯一标识一组关联日程事件
39 | :param single_events: 是否需要展开循环日程
40 | :param page_token: 查询对应页,值有上一次请求返回的结果里对应nextPageToken
41 | :param max_results: 结果返回的最多数量,默认250,最多返回2500
42 | :param time_max: 查询时间上限
43 | """
44 | return self._top_request(
45 | "dingtalk.oapi.calendar.list",
46 | {
47 | "user_id": user_id,
48 | "calendar_folder_id": calendar_folder_id,
49 | "time_min": time_min,
50 | "i_cal_uid": i_cal_uid,
51 | "single_events": single_events,
52 | "page_token": page_token,
53 | "max_results": max_results,
54 | "time_max": time_max
55 | }
56 | )
57 |
58 | def delete(self, userid='', calendar_id=''):
59 | """
60 | 日程删除
61 |
62 | :param userid: 员工id
63 | :param calendar_id: 日程id
64 | """
65 | return self._top_request(
66 | "dingtalk.oapi.calendar.delete",
67 | {
68 | "userid": userid,
69 | "calendar_id": calendar_id
70 | }
71 | )
72 |
--------------------------------------------------------------------------------
/dingtalk/client/api/callback.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 |
6 |
7 | class Callback(DingTalkBaseAPI):
8 | ALL_CALL_BACK_TAGS = (
9 | 'user_add_org', 'user_modify_org', 'user_leave_org', 'user_active_org',
10 | 'org_admin_add', 'org_admin_remove', 'org_dept_create', 'org_dept_modify',
11 | 'org_dept_remove', 'org_change', 'org_remove',
12 | 'label_user_change', 'label_conf_add', 'label_conf_modify', 'label_conf_del',
13 | 'edu_user_insert', 'edu_user_update', 'edu_user_delete',
14 | 'edu_user_relation_insert', 'edu_user_relation_update', 'edu_user_relation_delete',
15 | 'edu_dept_insert', 'edu_dept_update', 'edu_dept_delete',
16 | 'chat_add_member', 'chat_remove_member', 'chat_quit', 'chat_update_owner', 'chat_update_title', 'chat_disband',
17 | 'check_in', 'bpms_task_change', 'bpms_instance_change',
18 | 'attendance_check_record', 'attendance_schedule_change', 'attendance_overtime_duration',
19 | 'meetingroom_book', 'meetingroom_room_info'
20 | )
21 |
22 | def register_call_back(self, call_back_tags, token, aes_key, url):
23 | """
24 | 注册事件回调接口
25 |
26 | :param call_back_tags: 需要监听的事件类型
27 | :param token: 加解密需要用到的token
28 | :param aes_key: 数据加密密钥
29 | :param url: 接收事件回调的url
30 | :return:
31 | """
32 | call_back_tag = []
33 | for k in call_back_tags:
34 | if k in self.ALL_CALL_BACK_TAGS:
35 | call_back_tag.append(k)
36 | return self._post(
37 | '/call_back/register_call_back',
38 | {
39 | "call_back_tag": call_back_tag,
40 | "token": token,
41 | "aes_key": aes_key,
42 | "url": url
43 | }
44 | )
45 |
46 | def get_call_back(self):
47 | """
48 | 查询事件回调接口
49 |
50 | :return:
51 | """
52 | return self._get('/call_back/get_call_back')
53 |
54 | def update_call_back(self, call_back_tags, token, aes_key, url):
55 | """
56 | 更新事件回调接口
57 |
58 | :param call_back_tags: 需要监听的事件类型
59 | :param token: 加解密需要用到的token
60 | :param aes_key: 数据加密密钥
61 | :param url: 接收事件回调的url
62 | :return:
63 | """
64 | call_back_tag = []
65 | for k in call_back_tags:
66 | if k in self.ALL_CALL_BACK_TAGS:
67 | call_back_tag.append(k)
68 | return self._post(
69 | '/call_back/update_call_back',
70 | {
71 | "call_back_tag": call_back_tag,
72 | "token": token,
73 | "aes_key": aes_key,
74 | "url": url
75 | }
76 | )
77 |
78 | def delete_call_back(self):
79 | """
80 | 删除事件回调接口
81 |
82 | :return:
83 | """
84 | return self._get('/call_back/delete_call_back')
85 |
86 | def get_call_back_failed_result(self):
87 | """
88 | 获取回调失败的结果
89 |
90 | :return:
91 | """
92 | return self._get('/call_back/get_call_back_failed_result')
93 |
--------------------------------------------------------------------------------
/dingtalk/client/api/chat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 | from dingtalk.model.message import BodyBase
6 |
7 |
8 | class Chat(DingTalkBaseAPI):
9 |
10 | def create(self, name, owner, useridlist, show_history_type=False, searchable=0,
11 | validation_type=0, mention_all_authority=0, chat_banned_type=0, management_type=0):
12 | """
13 | 创建会话
14 |
15 | :param name: 群名称。长度限制为1~20个字符
16 | :param owner: 群主userId,员工唯一标识ID;必须为该会话useridlist的成员之一
17 | :param useridlist: 群成员列表,每次最多支持40人,群人数上限为1000
18 | :param show_history_type: 新成员是否可查看聊天历史消息(新成员入群是否可查看最近100条聊天记录)
19 | :param searchable: 群可搜索,0-默认,不可搜索,1-可搜索
20 | :param validation_type: 入群验证,0:不入群验证(默认) 1:入群验证
21 | :param mention_all_authority: @all 权限,0-默认,所有人,1-仅群主可@all
22 | :param chat_banned_type: 群禁言,0-默认,不禁言,1-全员禁言
23 | :param management_type: 管理类型,0-默认,所有人可管理,1-仅群主可管理
24 | :return: 群会话的id
25 | """
26 | return self._post(
27 | '/chat/create',
28 | {
29 | 'name': name,
30 | 'owner': owner,
31 | 'useridlist': useridlist,
32 | 'showHistoryType': 1 if show_history_type else 0,
33 | 'chatBannedType': chat_banned_type,
34 | 'searchable': searchable,
35 | 'validationType': validation_type,
36 | 'mentionAllAuthority': mention_all_authority,
37 | 'managementType': management_type
38 | },
39 | result_processor=lambda x: x['chatid']
40 | )
41 |
42 | def update(self, chatid, name=None, owner=None, add_useridlist=(), del_useridlist=(), icon='', chat_banned_type=0,
43 | searchable=0, validation_type=0, mention_all_authority=0, show_history_type=False, management_type=0):
44 | """
45 | 修改会话
46 |
47 | :param chatid: 群会话的id
48 | :param name: 群名称。长度限制为1~20个字符,不传则不修改
49 | :param owner: 群主userId,员工唯一标识ID;必须为该会话成员之一;不传则不修改
50 | :param add_useridlist: 添加成员列表,每次最多支持40人,群人数上限为1000
51 | :param del_useridlist: 删除成员列表,每次最多支持40人,群人数上限为1000
52 | :param icon: 群头像mediaid
53 | :param chat_banned_type: 群禁言,0-默认,不禁言,1-全员禁言
54 | :param searchable: 群可搜索,0-默认,不可搜索,1-可搜索
55 | :param validation_type: 入群验证,0:不入群验证(默认) 1:入群验证
56 | :param mention_all_authority: @all 权限,0-默认,所有人,1-仅群主可@all
57 | :param show_history_type: 新成员是否可查看聊天历史消息(新成员入群是否可查看最近100条聊天记录)
58 | :param management_type: 管理类型,0-默认,所有人可管理,1-仅群主可管理
59 | :return:
60 | """
61 | return self._post(
62 | '/chat/update',
63 | {
64 | 'chatid': chatid,
65 | 'name': name,
66 | 'owner': owner,
67 | 'add_useridlist': add_useridlist,
68 | 'del_useridlist': del_useridlist,
69 | 'icon': icon,
70 | 'chatBannedType': chat_banned_type,
71 | 'searchable': searchable,
72 | 'validationType': validation_type,
73 | 'mentionAllAuthority': mention_all_authority,
74 | 'showHistoryType': 1 if show_history_type else 0,
75 | 'managementType': management_type
76 | }
77 | )
78 |
79 | def get(self, chatid):
80 | """
81 | 获取会话
82 |
83 | :param chatid: 群会话的id
84 | :return: 群会话信息
85 | """
86 | return self._get(
87 | '/chat/get',
88 | {'chatid': chatid},
89 | result_processor=lambda x: x['chat_info']
90 | )
91 |
92 | def send(self, chatid, msg_body):
93 | """
94 | 发送群消息
95 |
96 | :param chatid: 群会话的id
97 | :param msg_body: BodyBase 消息体
98 | :return: 加密的消息id
99 | """
100 | if isinstance(msg_body, BodyBase):
101 | msg_body = msg_body.get_dict()
102 | msg_body['chatid'] = chatid
103 | return self._post(
104 | '/chat/send',
105 | msg_body,
106 | result_processor=lambda x: x['messageId']
107 | )
108 |
109 | def get_read_list(self, message_id, cursor=0, size=100):
110 | """
111 | 查询群消息已读人员列表
112 |
113 | :param message_id: 发送群消息接口返回的加密消息id
114 | :param cursor: 分页查询的游标,第一次传0,后续传返回结果中的next_cursor。返回结果中没有next_cursor时,表示没有后续的数据了
115 | :param size: 分页查询的大小,最大可以传100
116 | :return:
117 | """
118 | return self._get(
119 | '/chat/getReadList',
120 | {"messageId": message_id, "cursor": cursor, "size": size}
121 | )
122 |
--------------------------------------------------------------------------------
/dingtalk/client/api/checkin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import time
5 | import datetime
6 |
7 | import six
8 | from dingtalk.core.utils import to_text
9 |
10 | from dingtalk.client.api.base import DingTalkBaseAPI
11 |
12 |
13 | class Checkin(DingTalkBaseAPI):
14 |
15 | def record(self, department_id, start_time, end_time, offset=0, size=100, order_asc=True):
16 | """
17 | 获得签到数据
18 |
19 | :param department_id: 部门id(1 表示根部门)
20 | :param start_time: 开始时间
21 | :param end_time: 结束时间
22 | :param offset: 偏移量
23 | :param size: 分页大小
24 | :param order_asc: 是否正序排列
25 | :return:
26 | """
27 |
28 | if isinstance(start_time, (datetime.date, datetime.datetime)):
29 | start_time = int(time.mktime(start_time.timetuple()) * 1000)
30 |
31 | if isinstance(end_time, (datetime.date, datetime.datetime)):
32 | end_time = int(time.mktime(end_time.timetuple()) * 1000)
33 |
34 | assert isinstance(start_time, six.integer_types) and isinstance(end_time, six.integer_types)
35 | return self._get(
36 | '/checkin/record',
37 | {
38 | 'department_id': department_id,
39 | 'start_time': start_time,
40 | 'end_time': end_time,
41 | 'offset': offset,
42 | 'size': size,
43 | 'order_asc': 'asc' if order_asc else 'desc'
44 | },
45 | result_processor=lambda x: x['data']
46 | )
47 |
48 | def record_get(self, userid_list, start_time, end_time, offset=0, size=100):
49 | """
50 | 获取多个用户的签到记录 (如果是取1个人的数据,时间范围最大到10天,如果是取多个人的数据,时间范围最大1天。)
51 |
52 | :param userid_list: 需要查询的用户列表
53 | :param start_time: 起始时间
54 | :param end_time: 截止时间
55 | :param offset: 偏移量
56 | :param size: 分页大小
57 | :return:
58 | """
59 | if isinstance(start_time, (datetime.date, datetime.datetime)):
60 | start_time = int(time.mktime(start_time.timetuple()) * 1000)
61 |
62 | if isinstance(end_time, (datetime.date, datetime.datetime)):
63 | end_time = int(time.mktime(end_time.timetuple()) * 1000)
64 |
65 | assert isinstance(start_time, six.integer_types) and isinstance(end_time, six.integer_types)
66 | if isinstance(userid_list, (list, tuple, set)):
67 | userid_list = ','.join(map(to_text, userid_list))
68 |
69 | return self._top_request(
70 | 'dingtalk.smartwork.checkin.record.get',
71 | {
72 | 'userid_list': userid_list,
73 | 'start_time': start_time,
74 | 'end_time': end_time,
75 | 'offset': offset,
76 | 'size': size
77 | }
78 | )
79 |
--------------------------------------------------------------------------------
/dingtalk/client/api/cspace.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import six
5 | from dingtalk.core.utils import to_text, json_loads
6 |
7 | from dingtalk.client.api.base import DingTalkBaseAPI
8 |
9 |
10 | class Cspace(DingTalkBaseAPI):
11 |
12 | def add_to_single_chat(self, agent_id, userid, media_id, file_name):
13 | """
14 | 发送文件给指定用户
15 |
16 | :param agent_id: 文件发送者微应用的agentId
17 | :param userid: 文件接收人的userid
18 | :param media_id: 调用钉盘上传文件接口得到的mediaid
19 | :param file_name: 文件名(需包含含扩展名)
20 | :return:
21 | """
22 | return self._post(
23 | '/cspace/add_to_single_chat',
24 | params={
25 | 'agent_id': agent_id,
26 | 'userid': userid,
27 | 'media_id': media_id,
28 | 'file_name': file_name
29 | }
30 | )
31 |
32 | def add(self, code, media_id, space_id, folder_id, name, agent_id=None, overwrite=False):
33 | """
34 | 新增文件到用户钉盘
35 |
36 | :param code: 如果是微应用,code值为微应用免登授权码,如果是服务窗应用,code值为服务窗免登授权码
37 | code为临时授权码,只能消费一次,下次请求需要重新获取新的code。
38 | :param media_id: 调用钉盘上传文件接口得到的mediaid
39 | :param space_id: 调用云盘选择控件后获取的用户钉盘空间ID
40 | :param folder_id: 调用云盘选择控件后获取的用户钉盘文件夹ID
41 | :param name: 上传文件的名称,不能包含非法字符
42 | :param agent_id: 微应用的agentId
43 | :param overwrite: 到同名文件是否覆盖,若不覆盖,则会自动重命名本次新增的文件
44 | :return:
45 | """
46 | return self._get(
47 | '/cspace/add',
48 | {
49 | 'agent_id': agent_id,
50 | 'code': code,
51 | 'media_id': media_id,
52 | 'space_id': space_id,
53 | 'folder_id': folder_id,
54 | 'name': name,
55 | 'overwrite': overwrite
56 | },
57 | result_processor=lambda x:
58 | json_loads(x['dentry']) if isinstance(x['dentry'], six.string_types) else x['dentry']
59 | )
60 |
61 | def get_custom_space(self, domain=None, agent_id=None):
62 | """
63 | 获取企业下的自定义空间
64 |
65 | :param domain: 企业调用时传入,需要为10个字节以内的字符串,仅可包含字母和数字,大小写不敏感
66 | :param agent_id: ISV调用时传入,微应用agentId
67 | :return: 申请到的空间id
68 | """
69 | return self._get(
70 | '/cspace/get_custom_space',
71 | {
72 | 'agent_id': agent_id,
73 | 'domain': domain
74 | },
75 | result_processor=lambda x: x['spaceid']
76 | )
77 |
78 | def grant_custom_space(self, isdownload, userid, agent_id=None, domain=None, duration=30, path=None, fileids=()):
79 | """
80 | 授权用户访问企业下的自定义空间
81 |
82 | :param isdownload: 权限类型,true为下载,false为上传
83 | :param userid: 企业用户userid
84 | :param agent_id: ISV调用时传入,授权访问指定微应用的自定义空间
85 | :param domain: 企业调用时传入,授权访问该domain的自定义空间
86 | :param duration: 权限有效时间,有效范围为0~3600秒,超出此范围或不传默认为30秒
87 | :param path: 授权访问的路径
88 | :param fileids: 授权访问的文件id列表
89 | :return:
90 | """
91 | _type = 'download' if isdownload else 'add'
92 | fileids = ','.join(map(to_text, fileids))
93 | return self._get(
94 | '/cspace/grant_custom_space',
95 | {
96 | 'type': _type,
97 | 'agent_id': agent_id,
98 | 'domain': domain,
99 | 'userid': userid,
100 | 'duration': duration,
101 | 'path': path,
102 | 'fileids': fileids
103 | },
104 | result_processor=lambda x: x['spaceid']
105 | )
106 |
107 | def file_upload_transaction(self, agent_id, file_size, chunk_numbers, upload_id=None):
108 | """
109 | 开启/提交 文件上传事务
110 |
111 | :param agent_id: 微应用的agentId
112 | :param file_size: 文件大小
113 | :param chunk_numbers: 文件总块数
114 | :param upload_id: 上传事务id 不传该值为开启事务,传该值为提交事务
115 | :return: 开启事务:上传事务id; 提交事务:文件存储id
116 | """
117 | return self._get(
118 | '/file/upload/transaction',
119 | {
120 | 'agent_id': agent_id,
121 | 'file_size': file_size,
122 | 'chunk_numbers': chunk_numbers,
123 | 'upload_id': upload_id
124 | },
125 | result_processor=lambda x: x['upload_id'] if upload_id is None else x['media_id']
126 | )
127 |
128 | def file_upload_chunk(self, agent_id, upload_id, chunk_sequence, file_chunk):
129 | """
130 | 上传文件块
131 |
132 | :param agent_id: 微应用的agentId
133 | :param upload_id: 上传事务id
134 | :param chunk_sequence: 文件块号,从1开始计数
135 | :param file_chunk: 要上传的文件块,一个 File-object
136 | :return:
137 | """
138 | return self._post(
139 | '/file/upload/chunk',
140 | params={
141 | 'agent_id': agent_id,
142 | 'upload_id': upload_id,
143 | 'chunk_sequence': chunk_sequence
144 | },
145 | files={
146 | 'file': file_chunk
147 | }
148 | )
149 |
150 | def file_upload_single(self, agent_id, file_size, media_file):
151 | """
152 | 单步文件上传
153 |
154 | :param agent_id: 微应用的agentId
155 | :param file_size: 文件大小
156 | :param media_file: 要上传的文件,一个 File-object
157 | :return:
158 | """
159 | return self._post(
160 | '/file/upload/single',
161 | params={
162 | 'agent_id': agent_id,
163 | 'file_size': file_size
164 | },
165 | files={
166 | 'file': media_file
167 | },
168 | result_processor=lambda x: x['media_id']
169 | )
170 |
--------------------------------------------------------------------------------
/dingtalk/client/api/department.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 |
6 |
7 | class Department(DingTalkBaseAPI):
8 |
9 | def list_ids(self, _id=1):
10 | """
11 | 获取子部门ID列表
12 |
13 | :param _id: 父部门id(如果不传,默认部门为根部门,根部门ID为1)
14 | :return: 子部门ID列表数据
15 | """
16 | return self._get(
17 | '/department/list_ids',
18 | {'id': _id},
19 | result_processor=lambda x: x['sub_dept_id_list']
20 | )
21 |
22 | def list(self, _id=1, lang='zh_CN', fetch_child=False):
23 | """
24 | 获取部门列表
25 |
26 | :param _id: 父部门id(如果不传,默认部门为根部门,根部门ID为1)
27 | :param lang: 通讯录语言(默认zh_CN,未来会支持en_US)
28 | :param fetch_child: 是否递归部门的全部子部门,ISV微应用固定传递false。
29 | :return: 部门列表数据。以部门的order字段从小到大排列
30 | """
31 | return self._get(
32 | '/department/list',
33 | {'id': _id, 'lang': lang, 'fetch_child': fetch_child},
34 | result_processor=lambda x: x['department']
35 | )
36 |
37 | def get(self, _id, lang='zh_CN'):
38 | """
39 | 获取部门详情
40 |
41 | :param _id: 部门id
42 | :param lang: 通讯录语言(默认zh_CN,未来会支持en_US)
43 | :return: 部门列表数据。以部门的order字段从小到大排列
44 | """
45 | return self._get(
46 | '/department/get',
47 | {'id': _id, 'lang': lang}
48 | )
49 |
50 | def create(self, department_data):
51 | """
52 | 创建部门
53 |
54 | :param department_data: 部门信息
55 | :return: 创建的部门id
56 | """
57 | if 'id' in department_data:
58 | raise AttributeError('不能包含Id')
59 | return self._post(
60 | '/department/create',
61 | department_data,
62 | result_processor=lambda x: x['id']
63 | )
64 |
65 | def update(self, department_data):
66 | """
67 | 更新部门
68 |
69 | :param department_data: 部门信息
70 | :return: 已经更新的部门id
71 | """
72 | if 'id' not in department_data:
73 | raise AttributeError('必须包含Id')
74 | return self._post(
75 | '/department/update',
76 | department_data,
77 | result_processor=lambda x: x['id']
78 | )
79 |
80 | def delete(self, _id):
81 | """
82 | 删除部门
83 |
84 | :param _id: 部门id。(注:不能删除根部门;不能删除含有子部门、成员的部门)
85 | :return:
86 | """
87 | return self._get(
88 | '/department/delete',
89 | {'id': _id}
90 | )
91 |
92 | def list_parent_depts_by_dept(self, _id):
93 | """
94 | 查询部门的所有上级父部门路径
95 |
96 | :param _id: 希望查询的部门的id,包含查询的部门本身
97 | :return: 该部门的所有父部门id列表
98 | """
99 | return self._get(
100 | '/department/list_parent_depts_by_dept',
101 | {'id': _id},
102 | result_processor=lambda x: x['parentIds']
103 | )
104 |
105 | def list_parent_depts(self, user_id):
106 | """
107 | 查询指定用户的所有上级父部门路径
108 |
109 | :param user_id: 希望查询的用户的id
110 | :return: 按顺序依次为其所有父部门的ID,直到根部门
111 | """
112 | return self._get(
113 | '/department/list_parent_depts',
114 | {'userId': user_id},
115 | result_processor=lambda x: x['department']
116 | )
117 |
--------------------------------------------------------------------------------
/dingtalk/client/api/employeerm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import datetime
5 | import json
6 |
7 | from optionaldict import optionaldict
8 |
9 | from dingtalk.core.utils import to_text
10 | from dingtalk.client.api.base import DingTalkBaseAPI
11 |
12 |
13 | class Employeerm(DingTalkBaseAPI):
14 |
15 | DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
16 |
17 | def get(self, userid):
18 | """
19 | 获取智能人事员工花名册详细数据
20 |
21 | :param userid: 查询用户userid
22 | :return:
23 | """
24 | return self._top_request(
25 | 'dingtalk.corp.hrm.employee.get',
26 | {'userid': userid},
27 | result_processor=lambda x: x['group_list']
28 | )
29 |
30 | def list(self, userid_list, field_filter_list=()):
31 | """
32 | 批量获取员工花名册字段信息
33 | 智能人事业务,企业/ISV根据员工id批量访问员工花名册信息
34 |
35 | :param userid_list: 员工id列表
36 | :param field_filter_list: 需要获取的花名册字段信息
37 | """
38 | if isinstance(userid_list, (list, tuple, set)):
39 | userid_list = ','.join(map(to_text, userid_list))
40 | if isinstance(field_filter_list, (list, tuple, set)):
41 | field_filter_list = ','.join(map(to_text, field_filter_list))
42 | return self._top_request(
43 | "dingtalk.oapi.smartwork.hrm.employee.list",
44 | {
45 | "userid_list": userid_list,
46 | "field_filter_list": field_filter_list
47 | }
48 | )
49 |
50 | def querypreentry(self, offset=0, size=50):
51 | """
52 | 智能人事查询公司待入职员工列表
53 | 智能人事业务,企业/ISV分页查询公司待入职员工id列表
54 |
55 | :param offset: 分页起始值,默认0开始
56 | :param size: 分页大小,最大50
57 | """
58 | return self._top_request(
59 | "dingtalk.oapi.smartwork.hrm.employee.querypreentry",
60 | {
61 | "offset": offset,
62 | "size": size
63 | }
64 | )
65 |
66 | def queryonjob(self, status_list=(), offset=0, size=50):
67 | """
68 | 智能人事查询公司在职员工列表
69 | 智能人事业务,提供企业/ISV按在职状态分页查询公司在职员工id列表
70 |
71 | :param status_list: 在职员工子状态筛选。2,试用期;3,正式;5,待离职;-1,无状态
72 | :param offset: 分页起始值,默认0开始
73 | :param size: 分页大小,最大50
74 | """
75 | if isinstance(status_list, (list, tuple, set)):
76 | status_list = ','.join(map(to_text, status_list))
77 | return self._top_request(
78 | "dingtalk.oapi.smartwork.hrm.employee.queryonjob",
79 | {
80 | "status_list": status_list,
81 | "offset": offset,
82 | "size": size
83 | }
84 | )
85 |
86 | def querydimission(self, offset=0, size=50):
87 | """
88 | 智能人事查询公司离职员工列表
89 | 智能人事业务,提供企业/ISV分页查询公司离职员工id列表
90 |
91 | :param offset: 分页游标,从0开始。根据返回结果里的next_cursor是否为空来判断是否还有下一页,且再次调用时offset设置成next_cursor的值
92 | :param size: 分页大小,最大50
93 | """
94 | return self._top_request(
95 | "dingtalk.oapi.smartwork.hrm.employee.querydimission",
96 | {
97 | "offset": offset,
98 | "size": size
99 | }
100 | )
101 |
102 | def listdimission(self, userid_list=()):
103 | """
104 | 批量获取员工离职信息
105 | 根据传入的staffId列表,批量查询员工的离职信息
106 |
107 | :param userid_list: 员工id
108 | """
109 | if isinstance(userid_list, (list, tuple, set)):
110 | userid_list = ','.join(map(to_text, userid_list))
111 | return self._top_request(
112 | "dingtalk.oapi.smartwork.hrm.employee.listdimission",
113 | {
114 | "userid_list": userid_list
115 | }
116 | )
117 |
118 | def addpreentry(self, name, mobile, pre_entry_time=None, op_userid=None, extend_info=None):
119 | """
120 | 智能人事添加企业待入职员工
121 |
122 | :param name: 员工姓名
123 | :param mobile: 手机号
124 | :param pre_entry_time: 预期入职时间
125 | :param op_userid: 操作人userid
126 | :param extend_info: 扩展信息
127 | :return:
128 | """
129 | if isinstance(pre_entry_time, (datetime.date, datetime.datetime)):
130 | pre_entry_time = pre_entry_time.strftime(self.DATE_TIME_FORMAT)
131 | if isinstance(extend_info, dict):
132 | extend_info = json.dumps(extend_info)
133 |
134 | return self._top_request(
135 | "dingtalk.oapi.smartwork.hrm.employee.addpreentry",
136 | {
137 | "param": optionaldict({
138 | "name": name,
139 | "mobile": mobile,
140 | "pre_entry_time": pre_entry_time,
141 | "op_userid": op_userid,
142 | "extend_info": extend_info
143 | })
144 | }
145 | )
146 |
147 | def getdismissionlist(self, op_userid, current=1, page_size=100):
148 | """
149 | 获取离职人员信息
150 |
151 | :param op_userid: 操作人userid
152 | :param current: 第几页,从1开始
153 | :param page_size: 一页多少数据,在1-100之间
154 | :return:
155 | """
156 | return self._top_request(
157 | 'dingtalk.corp.hrm.employee.getdismissionlist',
158 | {'op_userid': op_userid, 'current': current, 'page_size': page_size},
159 | result_processor=lambda x: x['page']
160 | )
161 |
162 | def setuserworkdata(self, op_userid, userid, data_value, data_desc=None):
163 | """
164 | 更新用户绩效数据
165 |
166 | :param op_userid: 操作人userid,必须是拥有被操作人操作权限的管理员userid
167 | :param userid: 被操作人userid
168 | :param data_value: 数据值,可以为数值或者字符串
169 | :param data_desc: 数据项描述信息
170 | :return:
171 | """
172 | hrm_api_user_data_model = {'userid': userid, 'data_value': data_value, 'data_desc': data_desc}
173 | return self._top_request(
174 | 'dingtalk.corp.hrm.employee.getdismissionlist',
175 | {'op_userid': op_userid, 'hrm_api_user_data_model': hrm_api_user_data_model}
176 | )
177 |
178 | def modjobinfo(self, op_userid, userid, employee_type=None, employee_status=None, confirm_join_time=None,
179 | probation_period_type=None, regular_time=None, join_working_time=None, birth_time=None):
180 | """
181 | 更新员工工作信息
182 |
183 | :param op_userid: 操作人userid,必须是拥有被操作人操作权限的管理员userid
184 | :param userid: 被操作人userid
185 | :param employee_type: 员工类型(1:全职,2:兼职,3:实习,4:劳务派遣,5:退休返聘,6:劳务外包)
186 | :param employee_status: 员工状态(2:试用,3:正式)
187 | :param confirm_join_time: 入职日期
188 | :param probation_period_type: 试用期(1:无试用期,2:1个月,3:2个月,4:3个月,5:4个月,6:5个月,7:6个月,8:其他)
189 | :param regular_time: 转正时间
190 | :param join_working_time: 首次参加工作时间
191 | :param birth_time: 生日日期
192 | :return:
193 | """
194 | if confirm_join_time is not None and isinstance(confirm_join_time, (datetime.date, datetime.datetime)):
195 | confirm_join_time = confirm_join_time.strftime('%Y-%m-%d %H:%M:%S')
196 | if regular_time is not None and isinstance(regular_time, (datetime.date, datetime.datetime)):
197 | regular_time = regular_time.strftime('%Y-%m-%d %H:%M:%S')
198 | if join_working_time is not None and isinstance(join_working_time, (datetime.date, datetime.datetime)):
199 | join_working_time = join_working_time.strftime('%Y-%m-%d %H:%M:%S')
200 | if birth_time is not None and isinstance(birth_time, (datetime.date, datetime.datetime)):
201 | birth_time = birth_time.strftime('%Y-%m-%d %H:%M:%S')
202 | hrm_api_job_model = {
203 | 'userid': userid,
204 | 'employee_type': employee_type,
205 | 'employee_status': employee_status,
206 | 'confirm_join_time': confirm_join_time,
207 | 'probation_period_type': probation_period_type,
208 | 'regular_time': regular_time,
209 | 'join_working_time': join_working_time,
210 | 'birth_time': birth_time
211 | }
212 | return self._top_request(
213 | 'dingtalk.corp.hrm.employee.modjobinfo',
214 | {'op_userid': op_userid, 'hrm_api_job_model': hrm_api_job_model}
215 | )
216 |
--------------------------------------------------------------------------------
/dingtalk/client/api/ext.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import six
5 |
6 | from dingtalk.client.api.base import DingTalkBaseAPI
7 | from dingtalk.core.utils import json_loads
8 |
9 |
10 | class Ext(DingTalkBaseAPI):
11 |
12 | def listlabelgroups(self, offset=0, size=100):
13 | """
14 | 标签列表
15 |
16 | :param offset: 偏移位置
17 | :param size: 分页大小,最大100
18 | :return:
19 | """
20 | return self._top_request(
21 | 'dingtalk.corp.ext.listlabelgroups',
22 | {'offset': offset, 'size': size},
23 | result_processor=lambda x: json_loads(x) if isinstance(x, six.string_types) else x
24 | )
25 |
26 | def list(self, offset=0, size=100):
27 | """
28 | 外部联系人列表
29 |
30 | :param offset: 偏移位置
31 | :param size: 分页大小,最大100
32 | :return:
33 | """
34 | return self._top_request(
35 | 'dingtalk.corp.ext.list',
36 | {'offset': offset, 'size': size},
37 | result_processor=lambda x: json_loads(x) if isinstance(x, six.string_types) else x
38 | )
39 |
40 | def add(self, name, follower_userid, label_ids, mobile, state_code='86',
41 | title=None, share_deptids=(), remark=None, address=None, company_name=None, share_userids=()):
42 | """
43 | 添加企业外部联系人
44 |
45 | :param name: 名称
46 | :param follower_userid: 负责人userId
47 | :param state_code: 手机号国家码
48 | :param mobile: 手机号
49 | :param label_ids: 标签列表
50 | :param title: 职位
51 | :param share_deptids: 共享给的部门ID
52 | :param remark: 备注
53 | :param address: 地址
54 | :param company_name: 企业名
55 | :param share_userids: 共享给的员工userId列表
56 | :return:
57 | """
58 |
59 | return self._top_request(
60 | 'dingtalk.corp.ext.add',
61 | {
62 | 'contact': {
63 | 'name': name,
64 | 'follower_user': follower_userid,
65 | 'state_code': state_code,
66 | 'mobile': mobile,
67 | 'label_ids': label_ids,
68 | 'title': title,
69 | 'share_deptids': share_deptids,
70 | 'remark': remark,
71 | 'address': address,
72 | 'company_name': company_name,
73 | 'share_userid': share_userids
74 | }
75 | },
76 | result_processor=lambda x: x['userid']
77 | )
78 |
--------------------------------------------------------------------------------
/dingtalk/client/api/extcontact.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 |
6 |
7 | class ExtContact(DingTalkBaseAPI):
8 |
9 | def listlabelgroups(self, offset=0, size=100):
10 | """
11 | 获取外部联系人标签列表
12 |
13 | :param size: 分页大小,最大100
14 | :param offset: 偏移位置
15 | """
16 | return self._top_request(
17 | "dingtalk.oapi.extcontact.listlabelgroups",
18 | {"size": size, "offset": offset}
19 | )
20 |
21 | def list(self, offset=0, size=100):
22 | """
23 | 获取外部联系人列表
24 |
25 | :param size: 分页大小, 最大100
26 | :param offset: 偏移位置
27 | """
28 | return self._top_request(
29 | "dingtalk.oapi.extcontact.list",
30 | {"size": size, "offset": offset}
31 | )
32 |
33 | def get(self, user_id):
34 | """
35 | 获取企业外部联系人详情
36 |
37 | :param user_id: userId
38 | """
39 | return self._top_request(
40 | "dingtalk.oapi.extcontact.get",
41 | {"user_id": user_id}
42 | )
43 |
44 | def create(self, name, follower_user_id, label_ids, mobile, state_code='86',
45 | title=None, share_dept_ids=(), remark=None, address=None, company_name=None, share_user_ids=()):
46 | """
47 | 添加外部联系人
48 |
49 | :param name: 名称
50 | :param follower_user_id: 负责人userId
51 | :param state_code: 手机号国家码
52 | :param mobile: 手机号
53 | :param label_ids: 标签列表
54 | :param title: 职位
55 | :param share_dept_ids: 共享给的部门ID
56 | :param remark: 备注
57 | :param address: 地址
58 | :param company_name: 企业名
59 | :param share_user_ids: 共享给的员工userId列表
60 | :return:
61 | """
62 | if not isinstance(label_ids, (list, tuple, set)):
63 | label_ids = (label_ids, )
64 | if not isinstance(share_dept_ids, (list, tuple, set)):
65 | share_dept_ids = (share_dept_ids, )
66 | if not isinstance(share_user_ids, (list, tuple, set)):
67 | share_user_ids = (share_user_ids, )
68 | return self._top_request(
69 | "dingtalk.oapi.extcontact.create",
70 | {
71 | "contact": {
72 | 'name': name,
73 | 'follower_user_id': follower_user_id,
74 | 'state_code': state_code,
75 | 'mobile': mobile,
76 | 'label_ids': label_ids,
77 | 'title': title,
78 | 'share_dept_ids': share_dept_ids,
79 | 'remark': remark,
80 | 'address': address,
81 | 'company_name': company_name,
82 | 'share_user_ids': share_user_ids
83 | }
84 | },
85 | result_processor=lambda x: x['userid']
86 | )
87 |
88 | def update(self, user_id, name, follower_user_id, label_ids, mobile, state_code='86',
89 | title=None, share_dept_ids=(), remark=None, address=None, company_name=None, share_user_ids=()):
90 | """
91 | 更新外部联系人
92 |
93 | :param user_id: 该外部联系人的userId
94 | :param name: 名称
95 | :param follower_user_id: 负责人userId
96 | :param state_code: 手机号国家码
97 | :param mobile: 手机号
98 | :param label_ids: 标签列表
99 | :param title: 职位
100 | :param share_dept_ids: 共享给的部门ID
101 | :param remark: 备注
102 | :param address: 地址
103 | :param company_name: 企业名
104 | :param share_user_ids: 共享给的员工userId列表
105 | :return:
106 | """
107 | if not isinstance(label_ids, (list, tuple, set)):
108 | label_ids = (label_ids, )
109 | if not isinstance(share_dept_ids, (list, tuple, set)):
110 | share_dept_ids = (share_dept_ids, )
111 | if not isinstance(share_user_ids, (list, tuple, set)):
112 | share_user_ids = (share_user_ids, )
113 | return self._top_request(
114 | "dingtalk.oapi.extcontact.update",
115 | {
116 | "contact": {
117 | "user_id": user_id,
118 | 'name': name,
119 | 'follower_user_id': follower_user_id,
120 | 'state_code': state_code,
121 | 'mobile': mobile,
122 | 'label_ids': label_ids,
123 | 'title': title,
124 | 'share_dept_ids': share_dept_ids,
125 | 'remark': remark,
126 | 'address': address,
127 | 'company_name': company_name,
128 | 'share_user_ids': share_user_ids
129 | }
130 | }
131 | )
132 |
133 | def delete(self, user_id):
134 | """
135 | 删除外部联系人
136 |
137 | :param user_id: 用户id
138 | """
139 | return self._top_request(
140 | "dingtalk.oapi.extcontact.delete",
141 | {"user_id": user_id}
142 | )
143 |
--------------------------------------------------------------------------------
/dingtalk/client/api/health.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import datetime
5 |
6 | from dingtalk.client.api.base import DingTalkBaseAPI
7 | from dingtalk.core.utils import to_text
8 |
9 |
10 | class Health(DingTalkBaseAPI):
11 |
12 | def stepinfo_getuserstatus(self, userid):
13 | """
14 | 查询用户是否开启了钉钉运动
15 |
16 | :param userid: 用户id
17 | """
18 | return self._top_request(
19 | "dingtalk.oapi.health.stepinfo.getuserstatus",
20 | {"userid": userid},
21 | result_processor=lambda x: x['status']
22 | )
23 |
24 | def stepinfo_list(self, _type, object_id, stat_dates):
25 | """
26 | 获取个人或部门钉钉运动步数
27 |
28 | :param _type: 0表示取用户步数,1表示取部门步数
29 | :param object_id: 可以传入用户userid或者部门id
30 | :param stat_dates: 时间列表
31 | """
32 | if not isinstance(stat_dates, (list, tuple, set)):
33 | stat_dates = [stat_dates]
34 |
35 | stat_dates = ",".join(map(lambda x: x.strftime("%Y%m%d") if isinstance(x, datetime.date) else x, stat_dates))
36 | return self._top_request(
37 | "dingtalk.oapi.health.stepinfo.list",
38 | {
39 | "type": _type,
40 | "object_id": object_id,
41 | "stat_dates": stat_dates
42 | }
43 | )
44 |
45 | def stepinfo_listbyuserid(self, userids, stat_date):
46 | """
47 | 批量查询多个用户的钉钉运动步数
48 |
49 | :param userids: 员工userid列表,最多传50个
50 | :param stat_date: 时间
51 | """
52 | if isinstance(stat_date, datetime.date):
53 | stat_date = stat_date.strftime("%Y%m%d")
54 | if isinstance(userids, (list, tuple, set)):
55 | userids = ",".join(map(to_text, userids))
56 | return self._top_request(
57 | "dingtalk.oapi.health.stepinfo.listbyuserid",
58 | {
59 | "userids": userids,
60 | "stat_date": stat_date
61 | }
62 | )
63 |
--------------------------------------------------------------------------------
/dingtalk/client/api/message.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import json
5 |
6 | from optionaldict import optionaldict
7 |
8 | from dingtalk.core.utils import to_text
9 | from six.moves.urllib.parse import urlencode
10 |
11 | from dingtalk.client.api.base import DingTalkBaseAPI
12 | from dingtalk.model.message import BodyBase
13 |
14 |
15 | class Message(DingTalkBaseAPI):
16 |
17 | @staticmethod
18 | def get_pc_url(url, pc_slide=True):
19 | """
20 | 消息链接能在PC端打开
21 |
22 | :param url: 要打开的链接
23 | :param pc_slide: 如果为true代表在PC客户端打开,为false或者不写代表用浏览器打开
24 | :return:
25 | """
26 | params = {'url': url}
27 | if pc_slide:
28 | params['pc_slide'] = 'true'
29 | return "dingtalk://dingtalkclient/page/link?%s" % urlencode(params)
30 |
31 | def media_upload(self, media_type, media_file):
32 | """
33 | 上传媒体文件
34 |
35 | :param media_type: 媒体文件类型,分别有图片(image)、语音(voice)、普通文件(file)
36 | :param media_file: 要上传的文件,一个 File-object
37 | :return:
38 | """
39 | return self._post(
40 | '/media/upload',
41 | params={'type': media_type},
42 | files={'media': media_file}
43 | )
44 |
45 | def media_download_file(self, media_id):
46 | """
47 | 获取媒体文件
48 |
49 | :param media_id: 媒体文件的唯一标示
50 | :return: requests 的 Response 实例
51 | """
52 | return self._get(
53 | '/media/downloadFile',
54 | {'media_id': media_id}
55 | )
56 |
57 | def send_to_conversation(self, sender, cid, msg_body):
58 | """
59 | 发送普通消息
60 |
61 | :param sender: 消息发送者员工ID
62 | :param cid: 群消息或者个人聊天会话Id
63 | :param msg_body: BodyBase 消息体
64 | :return:
65 | """
66 | if isinstance(msg_body, BodyBase):
67 | msg_body = msg_body.get_dict()
68 | msg_body['sender'] = sender
69 | msg_body['cid'] = cid
70 | return self._post('/message/send_to_conversation', msg_body)
71 |
72 | def send(self, agentid, msg_body, touser_list=(), toparty_list=()):
73 | """
74 | 发送企业通知消息
75 |
76 | :param agentid: 企业应用id,这个值代表以哪个应用的名义发送消息
77 | :param msg_body: BodyBase 消息体
78 | :param touser_list: 员工id列表
79 | :param toparty_list: 部门id列表
80 | :return:
81 | """
82 |
83 | touser = "|".join(map(to_text, touser_list))
84 | toparty = "|".join(map(to_text, toparty_list))
85 | if isinstance(msg_body, BodyBase):
86 | msg_body = msg_body.get_dict()
87 | msg_body['touser'] = touser
88 | msg_body['toparty'] = toparty
89 | msg_body['agentid'] = agentid
90 | return self._post('/message/send', msg_body)
91 |
92 | def list_message_status(self, message_id):
93 | """
94 | 获取企业通知消息已读未读状态
95 |
96 | :param message_id: 消息id
97 | :return:
98 | """
99 | return self._post('/message/list_message_status', {"messageId": message_id})
100 |
101 | def send_by_code(self, code, msg_body):
102 | """
103 | 企业通知消息接口(用户反馈式)
104 |
105 | :param code: 用户操作产生的授权码
106 | :param msg_body: BodyBase 消息体
107 | :return:
108 | """
109 | if isinstance(msg_body, BodyBase):
110 | msg_body = msg_body.get_dict()
111 | msg_body['code'] = code
112 | return self._post('/message/sendByCode', msg_body)
113 |
114 | def asyncsend(self, msg_body, agent_id, userid_list=(), dept_id_list=(), to_all_user=False):
115 | """
116 | 企业会话消息异步发送
117 |
118 | :param msg_body: BodyBase 消息体
119 | :param agent_id: 微应用的id
120 | :param userid_list: 接收者的用户userid列表
121 | :param dept_id_list: 接收者的部门id列表
122 | :param to_all_user: 是否发送给企业全部用户
123 | :return: 任务id
124 | """
125 | userid_list = ",".join(map(to_text, userid_list))
126 | dept_id_list = ",".join(map(to_text, dept_id_list))
127 |
128 | if isinstance(msg_body, BodyBase):
129 | msg_body = msg_body.get_dict()
130 | msgtype = msg_body['msgtype']
131 | msgcontent = json.dumps(msg_body[msgtype])
132 | return self._top_request(
133 | 'dingtalk.corp.message.corpconversation.asyncsend',
134 | {
135 | 'msgtype': msgtype,
136 | 'agent_id': agent_id,
137 | 'msgcontent': msgcontent,
138 | 'userid_list': userid_list,
139 | 'dept_id_list': dept_id_list,
140 | 'to_all_user': to_all_user
141 | },
142 | result_processor=lambda x: x['task_id']
143 | )
144 |
145 | def asyncsend_v2(self, msg_body, agent_id, userid_list=(), dept_id_list=(), to_all_user=False):
146 | """
147 | 企业会话消息异步发送
148 |
149 | :param msg_body: BodyBase 消息体
150 | :param agent_id: 微应用的id
151 | :param userid_list: 接收者的用户userid列表
152 | :param dept_id_list: 接收者的部门id列表
153 | :param to_all_user: 是否发送给企业全部用户
154 | :return: 任务id
155 | """
156 | if isinstance(userid_list, (list, tuple, set)):
157 | userid_list = ",".join(map(to_text, userid_list))
158 | if isinstance(dept_id_list, (list, tuple, set)):
159 | dept_id_list = ",".join(map(to_text, dept_id_list))
160 | if not userid_list:
161 | userid_list = None
162 | if not dept_id_list:
163 | dept_id_list = None
164 | if isinstance(msg_body, BodyBase):
165 | msg_body = msg_body.get_dict()
166 | return self._top_request(
167 | 'dingtalk.oapi.message.corpconversation.asyncsend_v2',
168 | optionaldict({
169 | "msg": msg_body,
170 | 'agent_id': agent_id,
171 | 'userid_list': userid_list,
172 | 'dept_id_list': dept_id_list,
173 | 'to_all_user': 'true' if to_all_user else 'false'
174 | }),
175 | result_processor=lambda x: x['task_id']
176 | )
177 |
178 | def recall(self, agent_id, msg_task_id):
179 | """
180 | 撤回工作通知消息
181 |
182 | :param agent_id: 发送工作通知的微应用agentId
183 | :param msg_task_id: 发送工作通知返回的taskId
184 | """
185 | return self._top_request(
186 | "dingtalk.oapi.message.corpconversation.recall",
187 | {"agent_id": agent_id, "msg_task_id": msg_task_id}
188 | )
189 |
190 | def getsendprogress(self, agent_id, task_id):
191 | """
192 | 获取异步发送企业会话消息的发送进度
193 |
194 | :param agent_id: 发送消息时使用的微应用的id
195 | :param task_id: 发送消息时钉钉返回的任务id
196 | :return:
197 | """
198 | return self._top_request(
199 | 'dingtalk.corp.message.corpconversation.getsendprogress',
200 | {'agent_id': agent_id, 'task_id': task_id},
201 | result_processor=lambda x: x['progress']
202 | )
203 |
204 | def getsendresult(self, agent_id=None, task_id=None):
205 | """
206 | 获取异步向企业会话发送消息的结果
207 |
208 | :param agent_id: 微应用的agentid
209 | :param task_id: 异步任务的id
210 | :return:
211 | """
212 | return self._top_request(
213 | 'dingtalk.corp.message.corpconversation.getsendresult',
214 | {'agent_id': agent_id, 'task_id': task_id},
215 | result_processor=lambda x: x['send_result']
216 | )
217 |
218 | def asyncsendbycode(self, code, msg_body, agent_id, userid_list=(), dept_id_list=(), to_all_user=False):
219 | """
220 | 通过用户授权码异步向企业会话发送消息
221 |
222 | :param code: 用户操作产生的授权码
223 | :param msg_body: BodyBase 消息体
224 | :param agent_id: 微应用的id
225 | :param userid_list: 接收者的用户userid列表
226 | :param dept_id_list: 接收者的部门id列表
227 | :param to_all_user: 是否发送给企业全部用户
228 | :return: 任务id
229 | """
230 | userid_list = ",".join(map(to_text, userid_list))
231 | dept_id_list = ",".join(map(to_text, dept_id_list))
232 |
233 | if isinstance(msg_body, BodyBase):
234 | msg_body = msg_body.get_dict()
235 | msgtype = msg_body['msgtype']
236 | msgcontent = json.dumps(msg_body[msgtype])
237 |
238 | return self._top_request(
239 | 'dingtalk.corp.message.corpconversation.asyncsendbycode',
240 | {
241 | 'msgtype': msgtype,
242 | 'code': code,
243 | 'agent_id': agent_id,
244 | 'msgcontent': msgcontent,
245 | 'userid_list': userid_list,
246 | 'dept_id_list': dept_id_list,
247 | 'to_all_user': to_all_user
248 | },
249 | result_processor=lambda x: x['task_id']
250 | )
251 |
--------------------------------------------------------------------------------
/dingtalk/client/api/microapp.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 |
6 |
7 | class MicroApp(DingTalkBaseAPI):
8 |
9 | def create(self, app_icon, app_name, app_desc, homepage_url, pc_homepage_url=None, omp_link=None):
10 | """
11 | 创建微应用
12 |
13 | :param app_icon: 微应用的图标。需要调用上传接口将图标上传到钉钉服务器后获取到的mediaId
14 | :param app_name: 微应用的名称。长度限制为1~10个字符
15 | :param app_desc: 微应用的描述。长度限制为1~20个字符
16 | :param homepage_url: 微应用的移动端主页,必须以http开头或https开头
17 | :param pc_homepage_url: 微应用的PC端主页,必须以http开头或https开头,如果不为空则必须与homepageUrl的域名一致
18 | :param omp_link: 微应用的OA后台管理主页,必须以http开头或https开头。
19 | :return: 微应用实例化id
20 | """
21 | return self._post(
22 | '/microapp/create',
23 | {
24 | "appIcon": app_icon,
25 | "appName": app_name,
26 | "appDesc": app_desc,
27 | "homepageUrl": homepage_url,
28 | "pcHomepageUrl": pc_homepage_url,
29 | "ompLink": omp_link
30 | },
31 | result_processor=lambda x: x['agentId']
32 | )
33 |
34 | def update(self, agent_id, app_icon=None, app_name=None, app_desc=None,
35 | homepage_url=None, pc_homepage_url=None, omp_link=None):
36 | """
37 | 更新微应用
38 |
39 | :param agent_id: 微应用实例化id
40 | :param app_icon: 微应用的图标。需要调用上传接口将图标上传到钉钉服务器后获取到的mediaId
41 | :param app_name: 微应用的名称。长度限制为1~10个字符
42 | :param app_desc: 微应用的描述。长度限制为1~20个字符
43 | :param homepage_url: 微应用的移动端主页,必须以http开头或https开头
44 | :param pc_homepage_url: 微应用的PC端主页,必须以http开头或https开头,如果不为空则必须与homepageUrl的域名一致
45 | :param omp_link: 微应用的OA后台管理主页,必须以http开头或https开头。
46 | :return: 微应用实例化id
47 | """
48 | return self._post(
49 | '/microapp/update',
50 | {
51 | "agentId": agent_id,
52 | "appIcon": app_icon,
53 | "appName": app_name,
54 | "appDesc": app_desc,
55 | "homepageUrl": homepage_url,
56 | "pcHomepageUrl": pc_homepage_url,
57 | "ompLink": omp_link
58 | },
59 | result_processor=lambda x: x['agentId']
60 | )
61 |
62 | def delete(self, agent_id):
63 | """
64 | 删除微应用
65 |
66 | :param agent_id: 微应用实例化id,企业只能删除自建微应用
67 | :return:
68 | """
69 | return self._post(
70 | '/microapp/delete',
71 | {'agentId': agent_id}
72 | )
73 |
74 | def list(self):
75 | """
76 | 列出微应用
77 |
78 | :return: 微应用列表
79 | """
80 | return self._post(
81 | '/microapp/list',
82 | result_processor=lambda x: x['appList']
83 | )
84 |
85 | def list_by_userid(self, userid):
86 | """
87 | 列出员工可见的微应用
88 |
89 | :return: 微应用列表
90 | """
91 | return self._get(
92 | '/microapp/list_by_userid',
93 | {'userid': userid},
94 | result_processor=lambda x: x['appList']
95 | )
96 |
97 | def visible_scopes(self, agent_id):
98 | """
99 | 删除微应用
100 |
101 | :param agent_id: 需要查询的微应用实例化agentId
102 | :return:
103 | """
104 | return self._post(
105 | '/microapp/visible_scopes',
106 | {'agentId': agent_id}
107 | )
108 |
109 | def set_visible_scopes(self, agent_id, is_hidden=False, dept_visible_scopes=(), user_visible_scopes=()):
110 | """
111 | 设置微应用的可见范围
112 |
113 | :param agent_id: 微应用实例化id
114 | :param is_hidden: 是否仅限管理员可见,true代表仅限管理员可见
115 | :param dept_visible_scopes: 设置可见的部门id列表
116 | :param user_visible_scopes: 设置可见的员工id列表
117 | :return:
118 | """
119 | return self._post(
120 | '/microapp/set_visible_scopes',
121 | {
122 | "agentId": agent_id,
123 | "isHidden": is_hidden,
124 | "deptVisibleScopes": dept_visible_scopes,
125 | "userVisibleScopes": user_visible_scopes
126 | }
127 | )
128 |
--------------------------------------------------------------------------------
/dingtalk/client/api/report.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import datetime
5 | import time
6 |
7 | from dingtalk.client.api.base import DingTalkBaseAPI
8 |
9 |
10 | class Report(DingTalkBaseAPI):
11 |
12 | def list(self, start_time, end_time, cursor=0, size=20, template_name='', userid=''):
13 | """
14 | 查询企业员工发出的日志列表
15 |
16 | :param start_time: 查询起始时间
17 | :param end_time: 查询截止时间
18 | :param cursor: 查询游标,初始传入0,后续从上一次的返回值中获取
19 | :param size: 每页数据量
20 | :param template_name: 要查询的模板名称
21 | :param userid: 员工的userid
22 | """
23 | if isinstance(start_time, (datetime.date, datetime.datetime)):
24 | start_time = int(time.mktime(start_time.timetuple()) * 1000)
25 | if isinstance(end_time, (datetime.date, datetime.datetime)):
26 | end_time = int(time.mktime(end_time.timetuple()) * 1000)
27 | return self._top_request(
28 | "dingtalk.oapi.report.list",
29 | {
30 | "start_time": start_time,
31 | "end_time": end_time,
32 | "cursor": cursor,
33 | "size": size,
34 | "template_name": template_name,
35 | "userid": userid
36 | }
37 | )
38 |
39 | def statistics(self, report_id):
40 | """
41 | 获取日志统计数据
42 |
43 | :param report_id: 日志id
44 | """
45 | return self._top_request(
46 | "dingtalk.oapi.report.statistics",
47 | {
48 | "report_id": report_id
49 | }
50 | )
51 |
52 | def statistics_listbytype(self, report_id, _type, offset=0, size=100):
53 | """
54 | 根据类型获取日志相关人员列表
55 |
56 | :param report_id: 日志id
57 | :param _type: 查询类型 0:已读人员列表 1:评论人员列表 2:点赞人员列表
58 | :param offset: 分页查询的游标,最开始传0,后续传返回参数中的next_cursor值,默认值为0
59 | :param size: 分页参数,每页大小,最多传100,默认值为100
60 | """
61 | return self._top_request(
62 | "dingtalk.oapi.report.statistics.listbytype",
63 | {
64 | "report_id": report_id,
65 | "type": _type,
66 | "offset": offset,
67 | "size": size
68 | }
69 | )
70 |
71 | def receiver_list(self, report_id, offset=0, size=100):
72 | """
73 | 获取日志分享人员列表
74 |
75 | :param report_id: 日志id
76 | :param offset: 分页查询的游标,最开始传0,后续传返回参数中的next_cursor值,默认值为0
77 | :param size: 分页参数,每页大小,最多传100,默认值为100
78 | """
79 | return self._top_request(
80 | "dingtalk.oapi.report.receiver.list",
81 | {
82 | "report_id": report_id,
83 | "offset": offset,
84 | "size": size
85 | }
86 | )
87 |
88 | def comment_list(self, report_id, offset=0, size=20):
89 | """
90 | 获取日志评论详情
91 |
92 | :param report_id: 日志id
93 | :param offset: 分页查询的游标,最开始传0,后续传返回参数中的next_cursor值,默认值为0
94 | :param size: 分页参数,每页大小,最多传20,默认值为20
95 | """
96 | return self._top_request(
97 | "dingtalk.oapi.report.comment.list",
98 | {
99 | "report_id": report_id,
100 | "offset": offset,
101 | "size": size
102 | }
103 | )
104 |
105 | def getunreadcount(self, userid=''):
106 | """
107 | 查询企业员工的日志未读数
108 |
109 | :param userid: 员工id
110 | """
111 | return self._top_request(
112 | "dingtalk.oapi.report.getunreadcount",
113 | {"userid": userid},
114 | result_processor=lambda x: x['count']
115 | )
116 |
117 | def template_listbyuserid(self, userid='', offset=0, size=100):
118 | """
119 | 根据用户id获取可见的日志模板列表
120 |
121 | :param userid: 员工userId, 不传递表示获取所有日志模板
122 | :param offset: 分页游标,从0开始。根据返回结果里的next_cursor是否为空来判断是否还有下一页,且再次调用时offset设置成next_cursor的值
123 | :param size: 分页大小,最大可设置成100
124 | """
125 | return self._top_request(
126 | "dingtalk.oapi.report.template.listbyuserid",
127 | {
128 | "userid": userid,
129 | "offset": offset,
130 | "size": size
131 | }
132 | )
133 |
--------------------------------------------------------------------------------
/dingtalk/client/api/role.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.core.utils import to_text
5 |
6 | from dingtalk.client.api.base import DingTalkBaseAPI
7 |
8 |
9 | class Role(DingTalkBaseAPI):
10 |
11 | def simplelist(self, role_id, offset=0, size=20):
12 | """
13 | 获取角色的员工列表
14 |
15 | :param role_id: 角色ID
16 | :param offset: 分页大小
17 | :param size: 分页偏移
18 | :return:
19 | """
20 | return self._top_request(
21 | 'dingtalk.corp.role.simplelist',
22 | {'role_id': role_id, 'offset': offset, 'size': size}
23 | )
24 |
25 | def list(self, offset=0, size=20):
26 | """
27 | 获取企业角色列表
28 |
29 | :param offset: 分页大小
30 | :param size: 分页偏移
31 | :return:
32 | """
33 | return self._top_request(
34 | 'dingtalk.corp.role.list',
35 | {'offset': offset, 'size': size}
36 | )
37 |
38 | def addrolesforemps(self, rolelid_list, userid_list):
39 | """
40 | 批量为员工增加角色信息
41 |
42 | :param rolelid_list: 角色id list
43 | :param userid_list: 员工id list
44 | :return:
45 | """
46 | if isinstance(rolelid_list, (list, tuple, set)):
47 | rolelid_list = ','.join(map(to_text, rolelid_list))
48 | if isinstance(userid_list, (list, tuple, set)):
49 | userid_list = ','.join(map(to_text, userid_list))
50 | return self._top_request(
51 | 'dingtalk.corp.role.addrolesforemps',
52 | {'rolelid_list': rolelid_list, 'userid_list': userid_list}
53 | )
54 |
55 | def removerolesforemps(self, rolelid_list, userid_list):
56 | """
57 | 批量删除员工角的色信息
58 |
59 | :param rolelid_list: 角色id list
60 | :param userid_list: 员工id list
61 | :return:
62 | """
63 | if isinstance(rolelid_list, (list, tuple, set)):
64 | rolelid_list = ','.join(map(to_text, rolelid_list))
65 | if isinstance(userid_list, (list, tuple, set)):
66 | userid_list = ','.join(map(to_text, userid_list))
67 | return self._top_request(
68 | 'dingtalk.corp.role.removerolesforemps',
69 | {'rolelid_list': rolelid_list, 'userid_list': userid_list}
70 | )
71 |
72 | def deleterole(self, role_id):
73 | """
74 | 删除角色信息
75 |
76 | :param role_id: 角色id
77 | :return:
78 | """
79 | return self._top_request(
80 | 'dingtalk.corp.role.deleterole',
81 | {'role_id': role_id}
82 | )
83 |
84 | def getrolegroup(self, group_id):
85 | """
86 | 获取角色组信息
87 |
88 | :param group_id: 角色组的Id
89 | :return:
90 | """
91 | return self._top_request(
92 | 'dingtalk.corp.role.getrolegroup',
93 | {'group_id': group_id},
94 | result_processor=lambda x: x['role_group']
95 | )
96 |
97 | def getrole(self, role_id):
98 | """
99 | 获取角色详情
100 |
101 | :param role_id: 角色id
102 | """
103 | return self._top_request(
104 | "dingtalk.oapi.role.getrole",
105 | {
106 | "roleId": role_id
107 | }
108 | )
109 |
110 | def add_role(self, role_name, group_id):
111 | """
112 | 创建角色
113 |
114 | :param role_name: 角色名称
115 | :param group_id: 角色组id
116 | """
117 | return self.post(
118 | "/role/add_role",
119 | {
120 | "roleName": role_name,
121 | "groupId": group_id
122 | }
123 | )
124 |
125 | def update_role(self, role_name, group_id):
126 | """
127 | 更新角色
128 |
129 | :param role_name: 角色名称
130 | :param group_id: 角色组id
131 | """
132 | return self.post(
133 | "/role/update_role",
134 | {
135 | "roleName": role_name,
136 | "groupId": group_id
137 | }
138 | )
139 |
140 | def add_role_group(self, name):
141 | """
142 | 创建角色组
143 |
144 | :param name: 角色组名称
145 | """
146 | return self.post(
147 | "/role/add_role_group",
148 | {"name": name},
149 | result_processor=lambda x: x['groupId']
150 | )
151 |
--------------------------------------------------------------------------------
/dingtalk/client/api/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.client.api.base import DingTalkBaseAPI
5 |
6 |
7 | class User(DingTalkBaseAPI):
8 |
9 | def auth_scopes(self):
10 | """
11 | 获取CorpSecret授权范围
12 |
13 | :return:
14 | """
15 | return self._get('/auth/scopes')
16 |
17 | def get_org_user_count(self, only_active):
18 | """
19 | 获取企业员工人数
20 |
21 | :param only_active: 是否包含未激活钉钉的人员数量
22 | :return: 企业员工数量
23 | """
24 | return self._get(
25 | '/user/get_org_user_count',
26 | {'onlyActive': 0 if only_active else 1},
27 | result_processor=lambda x: x['count']
28 | )
29 |
30 | def getuserinfo(self, code):
31 | """
32 | 通过CODE换取用户身份
33 |
34 | :param code: requestAuthCode接口中获取的CODE
35 | :return:
36 | """
37 | return self._get(
38 | '/user/getuserinfo',
39 | {'code': code}
40 | )
41 |
42 | def get(self, userid, lang='zh_CN'):
43 | """
44 | 获取成员详情
45 |
46 | :param userid: 员工在企业内的UserID,企业用来唯一标识用户的字段
47 | :param lang: 通讯录语言(默认zh_CN,未来会支持en_US)
48 | :return:
49 | """
50 | return self._get(
51 | '/user/get',
52 | {'userid': userid, 'lang': lang}
53 | )
54 |
55 | def create(self, user_data):
56 | """
57 | 创建成员
58 |
59 | :param user_data: 用户信息
60 | :return: userid
61 | """
62 | return self._post(
63 | '/user/create',
64 | user_data,
65 | result_processor=lambda x: x['userid']
66 |
67 | )
68 |
69 | def update(self, user_data):
70 | """
71 | 更新成员
72 |
73 | :param user_data: 用户信息
74 | :return:
75 | """
76 | return self._post(
77 | '/user/update',
78 | user_data
79 | )
80 |
81 | def delete(self, userid):
82 | """
83 | 删除成员
84 |
85 | :param userid: 员工在企业内的UserID,企业用来唯一标识用户的字段
86 | :return:
87 | """
88 | return self._get(
89 | '/user/delete',
90 | {'userid': userid}
91 | )
92 |
93 | def batchdelete(self, user_ids):
94 | """
95 | 批量删除成员
96 |
97 | :param user_ids: 员工UserID列表。列表长度在1到20之间
98 | :return:
99 | """
100 | return self._post(
101 | '/user/delete',
102 | {'useridlist': list(user_ids)}
103 | )
104 |
105 | def simple_list(self, department_id, offset=0, size=100, order='custom', lang='zh_CN'):
106 | """
107 | 获取部门成员
108 |
109 | :param department_id: 获取的部门id
110 | :param offset: 偏移量
111 | :param size: 表分页大小,最大100
112 | :param order: 排序规则
113 | entry_asc 代表按照进入部门的时间升序
114 | entry_desc 代表按照进入部门的时间降序
115 | modify_asc 代表按照部门信息修改时间升序
116 | modify_desc 代表按照部门信息修改时间降序
117 | custom 代表用户定义排序
118 | :param lang: 通讯录语言(默认zh_CN另外支持en_US)
119 | :return:
120 | """
121 | return self._get(
122 | '/user/simplelist',
123 | {
124 | 'department_id': department_id,
125 | 'offset': offset,
126 | 'size': size,
127 | 'order': order,
128 | 'lang': lang
129 | }
130 | )
131 |
132 | def list(self, department_id, offset=0, size=100, order='custom', lang='zh_CN'):
133 | """
134 | 获取部门成员(详情)
135 |
136 | :param department_id: 获取的部门id
137 | :param offset: 偏移量
138 | :param size: 表分页大小,最大100
139 | :param order: 排序规则
140 | entry_asc 代表按照进入部门的时间升序
141 | entry_desc 代表按照进入部门的时间降序
142 | modify_asc 代表按照部门信息修改时间升序
143 | modify_desc 代表按照部门信息修改时间降序
144 | custom 代表用户定义排序
145 | :param lang: 通讯录语言(默认zh_CN另外支持en_US)
146 | :return:
147 | """
148 | return self._get(
149 | '/user/list',
150 | {
151 | 'department_id': department_id,
152 | 'offset': offset,
153 | 'size': size,
154 | 'order': order,
155 | 'lang': lang
156 | }
157 | )
158 |
159 | def get_admin(self):
160 | """
161 | 获取管理员列表
162 |
163 | :return: sys_level 管理员角色 1:主管理员,2:子管理员
164 | """
165 | return self._get(
166 | '/user/get_admin',
167 | result_processor=lambda x: x['admin_list']
168 | )
169 |
170 | def can_access_microapp(self, app_id, user_id):
171 | """
172 | 获取管理员的微应用管理权限
173 |
174 | :param app_id: 微应用id
175 | :param user_id: 员工唯一标识ID
176 | :return: 是否能管理该微应用
177 | """
178 | return self._get(
179 | '/user/can_access_microapp',
180 | {'appId': app_id, 'userId': user_id},
181 | result_processor=lambda x: x['canAccess']
182 | )
183 |
184 | def get_userid_by_unionid(self, unionid):
185 | """
186 | 根据unionid获取成员的userid
187 |
188 | :param unionid: 用户在当前钉钉开放平台账号范围内的唯一标识
189 | :return:
190 | """
191 | return self._get(
192 | '/user/getUseridByUnionid',
193 | {'unionid': unionid}
194 | )
195 |
196 | def get_dept_member(self, dept_id):
197 | """
198 | 获取部门用户userid列表
199 |
200 | :param dept_id: 用户在当前钉钉开放平台账号范围内的唯一标识
201 | :return 部门userid列表:
202 | """
203 | return self._get(
204 | '/user/getDeptMember',
205 | {'deptId': dept_id},
206 | result_processor=lambda x: x['userIds']
207 | )
208 |
209 | def listbypage(self, department_id, offset=0, size=100, order='custom', lang='zh_CN'):
210 | """
211 | 获取部门用户
212 |
213 | :param department_id: 获取的部门id
214 | :param offset: 偏移量
215 | :param size: 表分页大小,最大100
216 | :param order: 排序规则
217 | entry_asc 代表按照进入部门的时间升序
218 | entry_desc 代表按照进入部门的时间降序
219 | modify_asc 代表按照部门信息修改时间升序
220 | modify_desc 代表按照部门信息修改时间降序
221 | custom 代表用户定义排序
222 | :param lang: 通讯录语言(默认zh_CN另外支持en_US)
223 | :return:
224 | """
225 | return self._get(
226 | '/user/list',
227 | {
228 | 'department_id': department_id,
229 | 'offset': offset,
230 | 'size': size,
231 | 'order': order,
232 | 'lang': lang
233 | }
234 | )
235 |
236 | def get_admin_scope(self, userid):
237 | """
238 | 查询管理员通讯录权限范围
239 |
240 | :param userid: 用户id
241 | """
242 | return self._top_request(
243 | "dingtalk.oapi.user.get_admin_scope",
244 | {"userid": userid},
245 | result_processor=lambda x: x['dept_ids']
246 | )
247 |
--------------------------------------------------------------------------------
/dingtalk/client/api/workrecord.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import datetime
5 | import time
6 |
7 | from optionaldict import optionaldict
8 |
9 | from dingtalk.client.api.base import DingTalkBaseAPI
10 | from dingtalk.model.field import FieldBase
11 |
12 |
13 | class WorkRecord(DingTalkBaseAPI):
14 |
15 | def add(self, userid, create_time, title, url, form_item_dict, originator_user_id='', source_name=''):
16 | """
17 | 新增待办事项
18 |
19 | :param userid: 用户id
20 | :param create_time: 待办时间。Unix时间戳
21 | :param title: 标题
22 | :param url: 待办跳转url
23 | :param form_item_dict: 表单列表 OrderedDict((('标题1', '内容1'),('标题2', '内容2')))
24 | :param originator_user_id: manager7078
25 | :param source_name: 待办来源名称
26 | """
27 |
28 | if isinstance(create_time, (datetime.date, datetime.datetime)):
29 | create_time = int(time.mktime(create_time.timetuple()) * 1000)
30 | form_item_list = [{'title': k, 'content': v}for k, v in form_item_dict.items()]
31 | return self._top_request(
32 | "dingtalk.oapi.workrecord.add",
33 | {
34 | "userid": userid,
35 | "create_time": create_time,
36 | "title": title,
37 | "url": url,
38 | "formItemList": form_item_list,
39 | "originator_user_id": originator_user_id,
40 | "source_name": source_name
41 | },
42 | result_processor=lambda x: x['record_id']
43 | )
44 |
45 | def update(self, userid, record_id):
46 | """
47 | 更新待办事项状态
48 |
49 | :param userid: 用户id
50 | :param record_id: 待办事项唯一id
51 | """
52 | return self._top_request(
53 | "dingtalk.oapi.workrecord.update",
54 | {"userid": userid, "record_id": record_id}
55 | )
56 |
57 | def getbyuserid(self, userid, status, offset=0, limit=50):
58 | """
59 | 获取用户的待办事项
60 |
61 | :param userid: 用户唯一ID
62 | :param offset: 分页游标,从0开始,如返回结果中has_more为true,则表示还有数据,offset再传上一次的offset+limit
63 | :param limit: 分页大小,最多50
64 | :param status: 待办事项状态,0表示未完成,1表示完成
65 | """
66 | return self._top_request(
67 | "dingtalk.oapi.workrecord.getbyuserid",
68 | {
69 | "userid": userid,
70 | "offset": offset,
71 | "limit": limit,
72 | "status": status
73 | },
74 | result_processor=lambda x: x['records']
75 | )
76 |
77 | def process_save(self, name, description, form_component_list=(), process_code=None, agentid=None):
78 | """
79 | 保存审批模板
80 |
81 | :param name: 模板名称
82 | :param description: 模板描述
83 | :param form_component_list: 表单列表
84 | :param process_code: 模板的唯一码
85 | :param agentid: 企业微应用标识
86 | """
87 | form_component_list = [form.get_dict() if isinstance(form, FieldBase) else form for form in form_component_list]
88 |
89 | return self._top_request(
90 | "dingtalk.oapi.process.save",
91 | {
92 | "saveProcessRequest": optionaldict({
93 | "agentid": agentid,
94 | "process_code": process_code,
95 | "name": name,
96 | "description": description,
97 | "fake_mode": True,
98 | "form_component_list": form_component_list
99 | })
100 | },
101 | result_processor=lambda x: x['process_code']
102 | )
103 |
104 | def process_delete(self, process_code, agentid=''):
105 | """
106 | 删除创建的审批模板
107 |
108 | :param process_code: 模板的唯一码
109 | :param agentid: 微应用agentId,ISV必填
110 | """
111 | return self._top_request(
112 | "dingtalk.oapi.process.delete",
113 | {
114 | "request": {
115 | "process_code": process_code,
116 | "agentid": agentid
117 | }
118 | }
119 | )
120 |
121 | def process_workrecord_create(
122 | self,
123 | process_code,
124 | originator_user_id,
125 | form_component_values,
126 | url,
127 | agentid='',
128 | title=''
129 | ):
130 | """
131 | 发起不带流程的审批实例
132 |
133 | :param process_code: 审批模板唯一码
134 | :param originator_user_id: 审批发起人
135 | :param form_component_values: 表单参数列表
136 | :param url: 实例跳转链接
137 | :param agentid: 应用id
138 | :param title: 实例标题
139 | """
140 | if isinstance(form_component_values, dict):
141 | form_component_values = [{"name": name, "value": value} for name, value in form_component_values.items()]
142 | return self._top_request(
143 | "dingtalk.oapi.process.workrecord.create",
144 | {
145 | "request": {
146 | "process_code": process_code,
147 | "originator_user_id": originator_user_id,
148 | "form_component_values": form_component_values,
149 | "url": url,
150 | "agentid": agentid,
151 | "title": title
152 | }
153 | },
154 | result_processor=lambda x: x['process_instance_id']
155 | )
156 |
157 | def process_workrecord_update(
158 | self,
159 | process_instance_id,
160 | status,
161 | result,
162 | agentid=''
163 | ):
164 | """
165 | 同步待办实例状态
166 |
167 | :param process_instance_id: 实例id
168 | :param status: 实例状态,分为COMPLETED, TERMINATED
169 | :param result: 实例结果, 如果实例状态是COMPLETED,需要设置result,分为agree和refuse
170 | :param agentid: 应用id
171 | """
172 | return self._top_request(
173 | "dingtalk.oapi.process.workrecord.update",
174 | {
175 | "request": {
176 | "process_instance_id": process_instance_id,
177 | "status": status,
178 | "result": result,
179 | "agentid": agentid
180 | }
181 | }
182 | )
183 |
184 | def process_workrecord_task_create(
185 | self,
186 | process_instance_id,
187 | tasks,
188 | agentid='',
189 | activity_id=''
190 | ):
191 | """
192 | 创建待办任务
193 |
194 | :param process_instance_id: 实例id
195 | :param tasks: 任务列表
196 | :param agentid: 应用id
197 | :param activity_id: 节点id
198 | """
199 | return self._top_request(
200 | "dingtalk.oapi.process.workrecord.task.create",
201 | {
202 | "request": {
203 | "process_instance_id": process_instance_id,
204 | "tasks": tasks,
205 | "agentid": agentid,
206 | "activity_id": activity_id
207 | }
208 | },
209 | result_processor=lambda x: x['tasks']
210 | )
211 |
212 | def dingtalk_oapi_process_workrecord_task_update(
213 | self,
214 | process_instance_id,
215 | tasks,
216 | agentid=''
217 | ):
218 | """
219 | 更新待办任务状态
220 |
221 | :param process_instance_id: 实例id
222 | :param tasks: 任务列表
223 | :param agentid: 应用id
224 | """
225 | return self._top_request(
226 | "dingtalk.oapi.process.workrecord.task.update",
227 | {
228 | "request": {
229 | "process_instance_id": process_instance_id,
230 | "tasks": tasks,
231 | "agentid": agentid
232 | }
233 | },
234 | result_processor=lambda x: x['tasks']
235 | )
236 |
237 | def process_workrecord_taskgroup_cancel(
238 | self,
239 | process_instance_id,
240 | activity_id,
241 | agentid=''
242 | ):
243 | """
244 | 批量取消任务
245 |
246 | :param process_instance_id: 实例id
247 | :param activity_id: 任务组id
248 | :param agentid: 应用id
249 | """
250 | return self._top_request(
251 | "dingtalk.oapi.process.workrecord.taskgroup.cancel",
252 | {
253 | "request": {
254 | "process_instance_id": process_instance_id,
255 | "activity_id": activity_id,
256 | "agentid": agentid
257 | }
258 | }
259 | )
260 |
--------------------------------------------------------------------------------
/dingtalk/client/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import inspect
5 | import json
6 | import logging
7 | import requests
8 | import six
9 | from six.moves.urllib.parse import urljoin
10 |
11 | from dingtalk.client.api.base import DingTalkBaseAPI
12 | from dingtalk.core.exceptions import DingTalkClientException
13 | from dingtalk.core.utils import json_loads
14 | from dingtalk.storage.memorystorage import MemoryStorage
15 |
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | def _is_api_endpoint(obj):
21 | return isinstance(obj, DingTalkBaseAPI)
22 |
23 |
24 | class BaseClient(object):
25 |
26 | _http = requests.Session()
27 |
28 | API_BASE_URL = 'https://oapi.dingtalk.com/'
29 |
30 | def __new__(cls, *args, **kwargs):
31 | self = super(BaseClient, cls).__new__(cls)
32 | api_endpoints = inspect.getmembers(self, _is_api_endpoint)
33 | for name, api in api_endpoints:
34 | api_cls = type(api)
35 | api = api_cls(self)
36 | setattr(self, name, api)
37 | return self
38 |
39 | def __init__(self, storage=None, timeout=None, auto_retry=True):
40 | self.storage = storage or MemoryStorage()
41 | self.timeout = timeout
42 | self.auto_retry = auto_retry
43 |
44 | def _request(self, method, url_or_endpoint, **kwargs):
45 | if not url_or_endpoint.startswith(('http://', 'https://')):
46 | api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
47 | url = urljoin(api_base_url, url_or_endpoint)
48 | else:
49 | url = url_or_endpoint
50 |
51 | if 'params' not in kwargs:
52 | kwargs['params'] = {}
53 | if isinstance(kwargs.get('data', ''), dict):
54 | body = json.dumps(kwargs['data'], ensure_ascii=False)
55 | body = body.encode('utf-8')
56 | kwargs['data'] = body
57 | if 'headers' not in kwargs:
58 | kwargs['headers'] = {}
59 | kwargs['headers']['Content-Type'] = 'application/json'
60 |
61 | kwargs['timeout'] = kwargs.get('timeout', self.timeout)
62 | result_processor = kwargs.pop('result_processor', None)
63 | top_response_key = kwargs.pop('top_response_key', None)
64 | res = self._http.request(
65 | method=method,
66 | url=url,
67 | **kwargs
68 | )
69 | try:
70 | res.raise_for_status()
71 | except requests.RequestException as reqe:
72 | logger.error("\n【请求地址】: %s\n【请求参数】:%s \n%s\n【异常信息】:%s",
73 | url, kwargs.get('params', ''), kwargs.get('data', ''), reqe)
74 | raise DingTalkClientException(
75 | errcode=None,
76 | errmsg=None,
77 | client=self,
78 | request=reqe.request,
79 | response=reqe.response
80 | )
81 |
82 | result = self._handle_result(
83 | res, method, url, result_processor, top_response_key, **kwargs
84 | )
85 |
86 | logger.debug("\n【请求地址】: %s\n【请求参数】:%s \n%s\n【响应数据】:%s",
87 | url, kwargs.get('params', ''), kwargs.get('data', ''), result)
88 | return result
89 |
90 | def _decode_result(self, res):
91 | try:
92 | result = json_loads(res.content.decode('utf-8', 'ignore'), strict=False)
93 | except (TypeError, ValueError):
94 | # Return origin response object if we can not decode it as JSON
95 | logger.debug('Can not decode response as JSON', exc_info=True)
96 | return res
97 | return result
98 |
99 | def _handle_result(self, res, method=None, url=None, result_processor=None, top_response_key=None, **kwargs):
100 | if not isinstance(res, dict):
101 | # Dirty hack around asyncio based AsyncWeChatClient
102 | result = self._decode_result(res)
103 | else:
104 | result = res
105 |
106 | if not isinstance(result, dict):
107 | return result
108 | if top_response_key:
109 | if 'error_response' in result:
110 | error_response = result['error_response']
111 | logger.error("\n【请求地址】: %s\n【请求参数】:%s \n%s\n【错误信息】:%s",
112 | url, kwargs.get('params', ''), kwargs.get('data', ''), result)
113 | raise DingTalkClientException(
114 | error_response.get('code', -1),
115 | error_response.get('sub_msg', error_response.get('msg', '')),
116 | client=self,
117 | request=res.request,
118 | response=res
119 | )
120 | top_result = result
121 | if top_response_key in top_result:
122 | top_result = result[top_response_key]
123 | if 'result' in top_result:
124 | top_result = top_result['result']
125 | if isinstance(top_result, six.string_types):
126 | try:
127 | top_result = json_loads(top_result)
128 | except Exception:
129 | pass
130 | if isinstance(top_result, dict):
131 | if ('success' in top_result and not top_result['success']) or (
132 | 'is_success' in top_result and not top_result['is_success']):
133 | logger.error("\n【请求地址】: %s\n【请求参数】:%s \n%s\n【错误信息】:%s",
134 | url, kwargs.get('params', ''), kwargs.get('data', ''), result)
135 | raise DingTalkClientException(
136 | top_result.get('ding_open_errcode', -1),
137 | top_result.get('error_msg', ''),
138 | client=self,
139 | request=res.request,
140 | response=res
141 | )
142 | result = top_result
143 | if not isinstance(result, dict):
144 | return result
145 | if 'errcode' in result:
146 | result['errcode'] = int(result['errcode'])
147 |
148 | if 'errcode' in result and result['errcode'] != 0:
149 | errcode = result['errcode']
150 | errmsg = result.get('errmsg', errcode)
151 |
152 | logger.error("\n【请求地址】: %s\n【请求参数】:%s \n%s\n【错误信息】:%s",
153 | url, kwargs.get('params', ''), kwargs.get('data', ''), result)
154 | raise DingTalkClientException(
155 | errcode,
156 | errmsg,
157 | client=self,
158 | request=res.request,
159 | response=res
160 | )
161 |
162 | return result if not result_processor else result_processor(result)
163 |
164 | def _handle_pre_request(self, method, uri, kwargs):
165 | return method, uri, kwargs
166 |
167 | def _handle_pre_top_request(self, params, uri):
168 | if not uri.startswith(('http://', 'https://')):
169 | uri = urljoin('https://eco.taobao.com', uri)
170 | return params, uri
171 |
172 | def _handle_request_except(self, e, func, *args, **kwargs):
173 | raise e
174 |
175 | def request(self, method, uri, **kwargs):
176 | method, uri_with_access_token, kwargs = self._handle_pre_request(method, uri, kwargs)
177 | try:
178 | return self._request(method, uri_with_access_token, **kwargs)
179 | except DingTalkClientException as e:
180 | return self._handle_request_except(e, self.request, method, uri, **kwargs)
181 |
182 | def top_request(self, method, params=None, format_='json', v='2.0',
183 | simplify='false', partner_id=None, url=None, **kwargs):
184 | """
185 | top 接口请求
186 |
187 | :param method: API接口名称。
188 | :param params: 请求参数 (dict 格式)
189 | :param format_: 响应格式(默认json,如果使用xml,需要自己对返回结果解析)
190 | :param v: API协议版本,可选值:2.0。
191 | :param simplify: 是否采用精简JSON返回格式
192 | :param partner_id: 合作伙伴身份标识。
193 | :param url: 请求url,默认为 https://eco.taobao.com/router/rest
194 | """
195 | from datetime import datetime
196 |
197 | reqparams = {}
198 | if params is not None:
199 | for key, value in params.items():
200 | reqparams[key] = value if not isinstance(value, (dict, list, tuple)) else json.dumps(value)
201 | reqparams['method'] = method
202 | reqparams['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
203 | reqparams['format'] = format_
204 | reqparams['v'] = v
205 |
206 | if format_ == 'json':
207 | reqparams['simplify'] = simplify
208 | if partner_id:
209 | reqparams['partner_id'] = partner_id
210 | base_url = url or '/router/rest'
211 |
212 | reqparams, base_url = self._handle_pre_top_request(reqparams, base_url)
213 |
214 | if not base_url.startswith(('http://', 'https://')):
215 | base_url = urljoin(self.API_BASE_URL, base_url)
216 | response_key = method.replace('.', '_') + "_response"
217 | try:
218 | return self._request('POST', base_url, params=reqparams, top_response_key=response_key, **kwargs)
219 | except DingTalkClientException as e:
220 | return self._handle_request_except(e, self.request,
221 | method, format_, v, simplify, partner_id, url, params, **kwargs)
222 |
223 | def get(self, uri, params=None, **kwargs):
224 | """
225 | get 接口请求
226 |
227 | :param uri: 请求url
228 | :param params: get 参数(dict 格式)
229 | """
230 | if params is not None:
231 | kwargs['params'] = params
232 | return self.request('GET', uri, **kwargs)
233 |
234 | def post(self, uri, data=None, params=None, **kwargs):
235 | """
236 | post 接口请求
237 |
238 | :param uri: 请求url
239 | :param data: post 数据(dict 格式会自动转换为json)
240 | :param params: post接口中url问号后参数(dict 格式)
241 | """
242 | if data is not None:
243 | kwargs['data'] = data
244 | if params is not None:
245 | kwargs['params'] = params
246 | return self.request('POST', uri, **kwargs)
247 |
--------------------------------------------------------------------------------
/dingtalk/client/channel.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import logging
5 |
6 | import time
7 |
8 | from dingtalk.client.base import BaseClient
9 | from dingtalk.core.utils import random_string, DingTalkSigner
10 | from dingtalk.storage.cache import ChannelCache
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class ChannelClient(BaseClient):
16 |
17 | def __init__(self, corp_id, prefix='channel', storage=None, timeout=None, auto_retry=True):
18 | super(ChannelClient, self).__init__(storage, timeout, auto_retry)
19 | self.corp_id = corp_id
20 | self.cache = ChannelCache(self.storage, prefix)
21 |
22 | def _handle_pre_request(self, method, uri, kwargs):
23 | if 'access_token=' in uri or 'access_token' in kwargs.get('params', {}):
24 | raise ValueError("access_token: " + uri)
25 | uri = '%s%access_token=%s' % (uri, '&' if '?' in uri else '?', self.channel_token)
26 | return method, uri, kwargs
27 |
28 | def _handle_request_except(self, e, func, *args, **kwargs):
29 | if e.errcode in (33001, 40001, 42001, 40014):
30 | self.cache.channel_token.delete()
31 | if self.auto_retry:
32 | return func(*args, **kwargs)
33 | raise e
34 |
35 | @property
36 | def channel_token(self):
37 | self.cache.channel_token.get()
38 | token = self.cache.channel_token.get()
39 | if token is None:
40 | ret = self.get_channel_token()
41 | token = ret['access_token']
42 | expires_in = ret.get('expires_in', 7200)
43 | self.cache.channel_token.set(value=token, ttl=expires_in)
44 | return token
45 |
46 | @property
47 | def channel_jsapi_ticket(self):
48 | ticket = self.cache.jsapi_ticket.get()
49 | if ticket is None:
50 | ret = self.get_channel_jsapi_ticket()
51 | ticket = ret['ticket']
52 | expires_in = ret.get('expires_in', 7200)
53 | self.cache.jsapi_ticket.set(value=ticket, ttl=expires_in)
54 | return ticket
55 |
56 | def get_jsapi_params(self, url, noncestr=None, timestamp=None):
57 | if not noncestr:
58 | noncestr = random_string()
59 | if timestamp is None:
60 | timestamp = int(time.time() * 1000)
61 | data = [
62 | 'noncestr={noncestr}'.format(noncestr=noncestr),
63 | 'jsapi_ticket={ticket}'.format(ticket=self.channel_jsapi_ticket),
64 | 'timestamp={timestamp}'.format(timestamp=timestamp),
65 | 'url={url}'.format(url=url),
66 | ]
67 | signer = DingTalkSigner(delimiter=b'&')
68 | signer.add_data(*data)
69 |
70 | ret = {
71 | 'corpId': self.corp_id,
72 | 'timeStamp': timestamp,
73 | 'nonceStr': noncestr,
74 | 'signature': signer.signature
75 | }
76 | return ret
77 |
78 | def get_channel_token(self):
79 | raise NotImplementedError
80 |
81 | def get_channel_jsapi_ticket(self):
82 | """
83 | 获取企业服务窗JSAPI鉴权ticket
84 |
85 | :return:
86 | """
87 | return self.get('/channel/get_channel_jsapi_ticket')
88 |
89 | def get_user_list(self, offset=0, size=100):
90 | """
91 | 获取服务窗关注者列表
92 |
93 | :param offset: 偏移量,必须大于等于0
94 | :param size: 获取数量,大于等于0,小于等于100
95 | :return:
96 | """
97 | return self.get(
98 | '/channel/user/list',
99 | {"offset": offset, "size": size}
100 | )
101 |
102 | def get_by_openid(self, openid):
103 | """
104 | 获取关注者详情
105 |
106 | :param openid: 在本服务窗运营服务商 范围内,唯一标识关注者身份的id
107 | :return:
108 | """
109 | return self.get(
110 | '/channel/user/get_by_openid',
111 | {"openid": openid}
112 | )
113 |
114 | def get_by_code(self, code):
115 | """
116 | 关注者免登接口
117 |
118 | :param code: 服务窗关注者在服务窗应用中免登时生成的临时授权码
119 | :return:
120 | """
121 | return self.get(
122 | '/channel/user/get_by_code',
123 | {"code": code}
124 | )
125 |
126 |
127 | class SecretChannelClient(ChannelClient):
128 | def __init__(self, corp_id, channel_secret, storage=None, timeout=None, auto_retry=True):
129 | super(SecretChannelClient, self).__init__(corp_id, storage, timeout, auto_retry)
130 | self.channel_secret = channel_secret
131 | self.cache = ChannelCache(self.storage, 'channelsecret:' + self.corp_id)
132 |
133 | def get_channel_token(self):
134 | """
135 | 获取服务窗ChannelToken
136 |
137 | :return:
138 | """
139 | return self.get(
140 | '/channel/get_channel_token',
141 | {"corpid": self.corp_id, "channel_secret": self.channel_secret}
142 | )
143 |
--------------------------------------------------------------------------------
/dingtalk/client/isv.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import logging
5 |
6 | from dingtalk.core.utils import to_text, json_loads
7 | from dingtalk.client import DingTalkClient
8 | from dingtalk.client.base import BaseClient
9 | from dingtalk.client.channel import ChannelClient
10 | from dingtalk.core.constants import SuitePushType
11 | from dingtalk.crypto import DingTalkCrypto
12 | from dingtalk.storage.cache import ISVCache
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | class ISVDingTalkClient(DingTalkClient):
18 | def __init__(self, corp_id, isv_client):
19 | super(ISVDingTalkClient, self).__init__(corp_id, 'isv_auth:' + isv_client.suite_key,
20 | isv_client.storage, isv_client.timeout, isv_client.auto_retry)
21 | self.isv_client = isv_client
22 |
23 | def get_access_token(self):
24 | return self.isv_client.get_access_token_by_corpid(self.corp_id)
25 |
26 |
27 | class ISVChannelClient(ChannelClient):
28 | def __init__(self, corp_id, isv_client):
29 | super(ISVChannelClient, self).__init__(corp_id, 'isv_channel:' + isv_client.suite_key,
30 | isv_client.storage, isv_client.timeout, isv_client.auto_retry)
31 | self.isv_client = isv_client
32 |
33 | def get_channel_token(self):
34 | return self.isv_client.get_channel_token_by_corpid(self.corp_id)
35 |
36 |
37 | class ISVClient(BaseClient):
38 |
39 | def __init__(self, suite_key, suite_secret, token=None, aes_key=None, storage=None, timeout=None, auto_retry=True):
40 | super(ISVClient, self).__init__(storage, timeout, auto_retry)
41 | self.suite_key = suite_key
42 | self.suite_secret = suite_secret
43 | self.cache = ISVCache(self.storage, 'isv:' + self.suite_key)
44 | self.crypto = DingTalkCrypto(token, aes_key, suite_key)
45 |
46 | def _handle_pre_request(self, method, uri, kwargs):
47 | if 'suite_access_token=' in uri or 'suite_access_token' in kwargs.get('params', {}):
48 | raise ValueError("suite_access_token: " + uri)
49 | uri = '%s%ssuite_access_token=%s' % (uri, '&' if '?' in uri else '?', self.suite_access_token)
50 | return method, uri, kwargs
51 |
52 | def _handle_request_except(self, e, func, *args, **kwargs):
53 | if e.errcode in (33001, 40001, 42001, 40014):
54 | self.cache.suite_access_token.delete()
55 | if self.auto_retry:
56 | return func(*args, **kwargs)
57 | raise e
58 |
59 | def set_suite_ticket(self, suite_ticket):
60 | self.cache.suite_ticket.set(value=suite_ticket)
61 |
62 | @property
63 | def suite_access_token(self):
64 | self.cache.suite_access_token.get()
65 | token = self.cache.suite_access_token.get()
66 | if token is None:
67 | ret = self.get_suite_access_token()
68 | token = ret['suite_access_token']
69 | expires_in = ret.get('expires_in', 7200)
70 | self.cache.suite_access_token.set(value=token, ttl=expires_in)
71 | return token
72 |
73 | def _handle_permanent_code(self, permanent_code_data):
74 | permanent_code = permanent_code_data.get('permanent_code', None)
75 | ch_permanent_code = permanent_code_data.get('ch_permanent_code', None)
76 | corp_id = permanent_code_data.get('auth_corp_info', {}).get('corpid', None)
77 | if corp_id is None:
78 | return
79 | if permanent_code is not None:
80 | self.cache.permanent_code.set(corp_id, permanent_code)
81 | if ch_permanent_code is not None:
82 | self.cache.ch_permanent_code.set(corp_id, ch_permanent_code)
83 |
84 | def get_dingtalk_client(self, corp_id):
85 | return ISVDingTalkClient(corp_id, self)
86 |
87 | def get_channel_client(self, corp_id):
88 | return ISVChannelClient(corp_id, self)
89 |
90 | def proc_message(self, message):
91 | if not isinstance(message, dict):
92 | return
93 | event_type = message.get('EventType', None)
94 |
95 | if event_type == SuitePushType.SUITE_TICKET.value:
96 | suite_ticket = message.get('SuiteTicket', None)
97 | if suite_ticket:
98 | self.set_suite_ticket(suite_ticket)
99 | return
100 | elif event_type == SuitePushType.TMP_AUTH_CODE.value:
101 | auth_code = message.get('AuthCode')
102 | permanent_code_data = self.get_permanent_code(auth_code)
103 | message['__permanent_code_data'] = permanent_code_data
104 | return
105 | elif event_type == SuitePushType.SUITE_RELIEVE.value:
106 | corp_id = message.get('AuthCorpId')
107 | self.cache.permanent_code.delete(corp_id)
108 | self.cache.ch_permanent_code.delete(corp_id)
109 | return
110 | else:
111 | return
112 |
113 | def parse_message(self, msg, signature, timestamp, nonce):
114 | message = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
115 | try:
116 | message = json_loads(to_text(message))
117 | self.proc_message(message)
118 | except Exception as e:
119 | logger.error("proc_message error %s %s", message, e)
120 | return message
121 |
122 | def get_ch_permanent_code_from_cache(self, corp_id):
123 | return self.cache.ch_permanent_code.get(corp_id)
124 |
125 | def get_permanent_code_from_cache(self, corp_id):
126 | return self.cache.permanent_code.get(corp_id)
127 |
128 | def get_suite_access_token(self):
129 | """
130 | 获取应用套件令牌Token
131 |
132 | :return:
133 | """
134 | return self._request(
135 | 'post',
136 | '/service/get_suite_token',
137 | data={
138 | "suite_key": self.suite_key,
139 | "suite_secret": self.suite_secret,
140 | "suite_ticket": self.cache.suite_ticket.get()
141 | }
142 | )
143 |
144 | def get_permanent_code(self, tmp_auth_code):
145 | """
146 | 获取企业授权的永久授权码
147 |
148 | :param tmp_auth_code: 回调接口(tmp_auth_code)获取的临时授权码
149 | :return:
150 | """
151 | permanent_code_data = self.post(
152 | '/service/get_permanent_code',
153 | {'tmp_auth_code': tmp_auth_code}
154 | )
155 | self._handle_permanent_code(permanent_code_data)
156 | return permanent_code_data
157 |
158 | def activate_suite(self, corp_id):
159 | """
160 | 激活套件
161 |
162 | :param corp_id: 授权方corpid
163 | :return:
164 | """
165 | return self.post(
166 | '/service/activate_suite',
167 | {
168 | 'suite_key': self.suite_key,
169 | 'auth_corpid': corp_id,
170 | 'permanent_code': self.cache.permanent_code.get(corp_id)}
171 | )
172 |
173 | def get_access_token_by_corpid(self, corp_id):
174 | """
175 | 获取企业授权的凭证
176 |
177 | :param corp_id: 授权方corpid
178 | :return:
179 | """
180 | return self.post(
181 | '/service/get_corp_token',
182 | {'auth_corpid': corp_id, 'permanent_code': self.cache.permanent_code.get(corp_id)}
183 | )
184 |
185 | def get_auth_info(self, corp_id):
186 | """
187 | 获取企业基本信息
188 |
189 | :param corp_id: 授权方corpid
190 | :return:
191 | """
192 | return self.post(
193 | '/service/get_auth_info',
194 | {'auth_corpid': corp_id, 'suite_key': self.suite_key}
195 | )
196 |
197 | def get_agent(self, corp_id, agent_id):
198 | """
199 | 获取企业的应用信息
200 |
201 | :param corp_id: 授权方corpid
202 | :param agent_id: 授权方应用id
203 | :return:
204 | """
205 | return self.post(
206 | '/service/get_agent',
207 | {
208 | 'suite_key': self.suite_key,
209 | 'auth_corpid': corp_id,
210 | 'agentid': agent_id,
211 | 'permanent_code': self.get_permanent_code_from_cache(corp_id)
212 | }
213 | )
214 |
215 | def get_unactive_corp(self, app_id):
216 | """
217 | 获取应用未激活的企业列表
218 |
219 | :param app_id: 套件下的微应用ID
220 | :return:
221 | """
222 | return self.post(
223 | '/service/get_unactive_corp',
224 | {'app_id': app_id}
225 | )
226 |
227 | def reauth_corp(self, app_id, corpid_list):
228 | """
229 | 重新授权未激活应用的企业
230 |
231 | :param app_id: 套件下的微应用ID
232 | :param corpid_list: 未激活的corpid列表
233 | :return:
234 | """
235 | return self.post(
236 | '/service/reauth_corp',
237 | {'app_id': app_id, 'corpid_list': corpid_list}
238 | )
239 |
240 | def set_corp_ipwhitelist(self, corp_id, ip_whitelist):
241 | """
242 | ISV为授权方的企业单独设置IP白名单
243 |
244 | :param corp_id: 授权方corpid
245 | :param ip_whitelist: 要为其设置的IP白名单,格式支持IP段,用星号表示,注意:仅支持后两段设置为星号
246 | :return:
247 | """
248 | return self.post(
249 | '/service/set_corp_ipwhitelist',
250 | {'auth_corpid': corp_id, 'ip_whitelist': ip_whitelist}
251 | )
252 |
253 | def get_channel_token_by_corpid(self, corp_id):
254 | """
255 | ISV获取企业服务窗接口调用TOKEN
256 |
257 | :param corp_id: 授权方corpid
258 | :return:
259 | """
260 | return self.post(
261 | '/service/get_channel_corp_token',
262 | {'auth_corpid': corp_id, 'ch_permanent_code': self.get_ch_permanent_code_from_cache(corp_id)}
263 | )
264 |
--------------------------------------------------------------------------------
/dingtalk/core/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
--------------------------------------------------------------------------------
/dingtalk/core/constants.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from enum import Enum
5 |
6 |
7 | class SuitePushType(Enum):
8 | """套件相关回调枚举"""
9 | CHECK_URL = "check_url" # 校验url
10 | CHANGE_AUTH = "change_auth" # 授权变更
11 | SUITE_TICKET = "suite_ticket" # 套件ticket
12 | TMP_AUTH_CODE = "tmp_auth_code" # 临时授权码
13 | SUITE_RELIEVE = "suite_relieve" # 解除授权
14 | CHECK_CREATE_SUITE_URL = "check_create_suite_url" # 校验创建套件时候的url
15 | CHECK_UPDATE_SUITE_URL = "check_update_suite_url" # 校验更改套件时候的url
16 | CHECK_SUITE_LICENSE_CODE = "check_suite_license_code" # 校验序列号
17 | MARKET_BUY = "market_buy" # 用户购买下单
18 | ORG_MICRO_APP_STOP = "org_micro_app_stop" # 企业逻辑停用微应用
19 | ORG_MICRO_APP_REMOVE = "org_micro_app_remove" # 企业物理删除微应用
20 | ORG_MICRO_APP_RESTORE = "org_micro_app_restore" # 企业逻辑启用微应用
21 |
--------------------------------------------------------------------------------
/dingtalk/core/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import six
5 |
6 | from dingtalk.core.utils import to_binary, to_text
7 |
8 |
9 | class DingTalkException(Exception):
10 |
11 | def __init__(self, errcode, errmsg):
12 | """
13 | :param errcode: Error code
14 | :param errmsg: Error message
15 | """
16 | self.errcode = errcode
17 | self.errmsg = errmsg
18 |
19 | def __str__(self):
20 | _repr = 'Error code: {code}, message: {msg}'.format(
21 | code=self.errcode,
22 | msg=self.errmsg
23 | )
24 |
25 | if six.PY2:
26 | return to_binary(_repr)
27 | else:
28 | return to_text(_repr)
29 |
30 | def __repr__(self):
31 | _repr = '{klass}({code}, {msg})'.format(
32 | klass=self.__class__.__name__,
33 | code=self.errcode,
34 | msg=self.errmsg
35 | )
36 | if six.PY2:
37 | return to_binary(_repr)
38 | else:
39 | return to_text(_repr)
40 |
41 |
42 | class DingTalkClientException(DingTalkException):
43 | """WeChat API client exception class"""
44 | def __init__(self, errcode, errmsg, client=None,
45 | request=None, response=None):
46 | super(DingTalkClientException, self).__init__(errcode, errmsg)
47 | self.client = client
48 | self.request = request
49 | self.response = response
50 |
51 |
52 | class InvalidSignatureException(DingTalkException):
53 | """Invalid signature exception class"""
54 |
55 | def __init__(self, errcode=-40001, errmsg='Invalid signature'):
56 | super(InvalidSignatureException, self).__init__(errcode, errmsg)
57 |
58 |
59 | class InvalidCorpIdOrSuiteKeyException(DingTalkException):
60 | """Invalid app_id exception class"""
61 |
62 | def __init__(self, errcode=-40005, errmsg='Invalid CorpIdOrSuiteKey'):
63 | super(InvalidCorpIdOrSuiteKeyException, self).__init__(errcode, errmsg)
64 |
--------------------------------------------------------------------------------
/dingtalk/core/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import hashlib
5 | import json
6 | import random
7 | import string
8 |
9 | import six
10 |
11 |
12 | class ObjectDict(dict):
13 | """Makes a dictionary behave like an object, with attribute-style access.
14 | """
15 |
16 | def __getattr__(self, key):
17 | if key in self:
18 | return self[key]
19 | return None
20 |
21 | def __setattr__(self, key, value):
22 | self[key] = value
23 |
24 |
25 | class DingTalkSigner(object):
26 | """DingTalk data signer"""
27 |
28 | def __init__(self, delimiter=b''):
29 | self._data = []
30 | self._delimiter = to_binary(delimiter)
31 |
32 | def add_data(self, *args):
33 | """Add data to signer"""
34 | for data in args:
35 | self._data.append(to_binary(data))
36 |
37 | @property
38 | def signature(self):
39 | """Get data signature"""
40 | self._data.sort()
41 | str_to_sign = self._delimiter.join(self._data)
42 | return hashlib.sha1(str_to_sign).hexdigest()
43 |
44 |
45 | def to_text(value, encoding='utf-8'):
46 | """Convert value to unicode, default encoding is utf-8
47 |
48 | :param value: Value to be converted
49 | :param encoding: Desired encoding
50 | """
51 | if not value:
52 | return ''
53 | if isinstance(value, six.text_type):
54 | return value
55 | if isinstance(value, six.binary_type):
56 | return value.decode(encoding)
57 | return six.text_type(value)
58 |
59 |
60 | def to_binary(value, encoding='utf-8'):
61 | """Convert value to binary string, default encoding is utf-8
62 |
63 | :param value: Value to be converted
64 | :param encoding: Desired encoding
65 | """
66 | if not value:
67 | return b''
68 | if isinstance(value, six.binary_type):
69 | return value
70 | if isinstance(value, six.text_type):
71 | return value.encode(encoding)
72 | return to_text(value).encode(encoding)
73 |
74 |
75 | def random_string(length=16):
76 | rule = string.ascii_letters + string.digits
77 | rand_list = random.sample(rule, length)
78 | return ''.join(rand_list)
79 |
80 |
81 | def byte2int(c):
82 | if six.PY2:
83 | return ord(c)
84 | return c
85 |
86 |
87 | def json_loads(s, object_hook=ObjectDict, **kwargs):
88 | return json.loads(s, object_hook=object_hook, **kwargs)
89 |
--------------------------------------------------------------------------------
/dingtalk/crypto/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | import time
4 | import base64
5 | import json
6 |
7 | from dingtalk.core.exceptions import InvalidSignatureException, InvalidCorpIdOrSuiteKeyException
8 | from dingtalk.core.utils import to_text, to_binary, DingTalkSigner, random_string
9 |
10 | from dingtalk.crypto.base import BasePrpCrypto
11 |
12 |
13 | def _get_signature(token, timestamp, nonce, encrypt):
14 | signer = DingTalkSigner()
15 | signer.add_data(token, timestamp, nonce, encrypt)
16 | return signer.signature
17 |
18 |
19 | class PrpCrypto(BasePrpCrypto):
20 |
21 | def encrypt(self, text, _id):
22 | return self._encrypt(text, _id)
23 |
24 | def decrypt(self, text, _id):
25 | return self._decrypt(text, _id, InvalidCorpIdOrSuiteKeyException)
26 |
27 |
28 | class BaseDingTalkCrypto(object):
29 |
30 | def __init__(self, token, encoding_aes_key, _id):
31 | if encoding_aes_key:
32 | encoding_aes_key = to_binary(encoding_aes_key + '=')
33 | self.key = base64.b64decode(encoding_aes_key)
34 | assert len(self.key) == 32
35 | else:
36 | self.key = None
37 | self.token = token
38 | self._id = _id
39 |
40 | def _decrypt_encrypt_str(self,
41 | signature,
42 | timestamp,
43 | nonce,
44 | encrypt_str,
45 | crypto_class=None):
46 |
47 | _signature = _get_signature(self.token, timestamp, nonce, encrypt_str)
48 | if _signature != signature:
49 | raise InvalidSignatureException()
50 | assert self.key is not None
51 | pc = crypto_class(self.key)
52 | return pc.decrypt(encrypt_str, self._id)
53 |
54 | def _encrypt_message(self,
55 | msg,
56 | nonce=None,
57 | timestamp=None,
58 | crypto_class=None):
59 |
60 | timestamp = timestamp or int(time.time() * 1000)
61 | timestamp = to_text(timestamp)
62 | if nonce is None:
63 | nonce = random_string()
64 | assert self.key is not None
65 | pc = crypto_class(self.key)
66 | encrypt = to_text(pc.encrypt(msg, self._id))
67 | signature = _get_signature(self.token, timestamp, nonce, encrypt)
68 | result = dict()
69 | result['msg_signature'] = signature
70 | result['encrypt'] = encrypt
71 | result['timeStamp'] = timestamp
72 | result['nonce'] = nonce
73 | return result
74 |
75 | def _decrypt_message(self,
76 | msg,
77 | signature,
78 | timestamp,
79 | nonce,
80 | crypto_class=None):
81 | if not isinstance(msg, dict):
82 | msg = json.loads(to_text(msg))
83 | encrypt = msg['encrypt']
84 | return self._decrypt_encrypt_str(signature, timestamp, nonce, encrypt, crypto_class)
85 |
86 |
87 | class DingTalkCrypto(BaseDingTalkCrypto):
88 |
89 | def __init__(self, token, encoding_aes_key, corpid_or_suitekey):
90 | super(DingTalkCrypto, self).__init__(token, encoding_aes_key, corpid_or_suitekey)
91 |
92 | def decrypt_encrypt_str(self, signature, timestamp, nonce, encrypt_str):
93 | return self._decrypt_encrypt_str(signature, timestamp, nonce, encrypt_str, PrpCrypto)
94 |
95 | def encrypt_message(self, msg, nonce=None, timestamp=None):
96 | return self._encrypt_message(msg, nonce, timestamp, PrpCrypto)
97 |
98 | def decrypt_message(self, msg, signature, timestamp, nonce):
99 | return self._decrypt_message(
100 | msg,
101 | signature,
102 | timestamp,
103 | nonce,
104 | PrpCrypto
105 | )
106 |
--------------------------------------------------------------------------------
/dingtalk/crypto/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | import struct
4 | import socket
5 | import base64
6 |
7 | from dingtalk.core.utils import to_text, to_binary, random_string, byte2int
8 | from dingtalk.crypto.pkcs7 import PKCS7Encoder
9 | try:
10 | from dingtalk.crypto.cryptography import DingTalkCipher
11 | except ImportError:
12 | try:
13 | from dingtalk.crypto.pycrypto import DingTalkCipher
14 | except ImportError:
15 | raise Exception('You must install either cryptography or PyCrypto!')
16 |
17 |
18 | class BasePrpCrypto(object):
19 |
20 | def __init__(self, key):
21 | self.cipher = DingTalkCipher(key)
22 |
23 | def get_random_string(self):
24 | return random_string(16)
25 |
26 | def _encrypt(self, text, _id):
27 | text = to_binary(text)
28 | tmp_list = []
29 | tmp_list.append(to_binary(self.get_random_string()))
30 | length = struct.pack(b'I', socket.htonl(len(text)))
31 | tmp_list.append(length)
32 | tmp_list.append(text)
33 | tmp_list.append(to_binary(_id))
34 |
35 | text = b''.join(tmp_list)
36 | text = PKCS7Encoder.encode(text)
37 |
38 | ciphertext = to_binary(self.cipher.encrypt(text))
39 | return base64.b64encode(ciphertext)
40 |
41 | def _decrypt(self, text, _id, exception=None):
42 | text = to_binary(text)
43 | plain_text = self.cipher.decrypt(base64.b64decode(text))
44 | padding = byte2int(plain_text[-1])
45 | content = plain_text[16:-padding]
46 | xml_length = socket.ntohl(struct.unpack(b'I', content[:4])[0])
47 | xml_content = to_text(content[4:xml_length + 4])
48 | from_id = to_text(content[xml_length + 4:])
49 | if from_id != _id:
50 | exception = exception or Exception
51 | raise exception()
52 | return xml_content
53 |
--------------------------------------------------------------------------------
/dingtalk/crypto/cryptography.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
4 | from cryptography.hazmat.backends import default_backend
5 |
6 |
7 | class DingTalkCipher(object):
8 |
9 | def __init__(self, key):
10 | backend = default_backend()
11 | self.cipher = Cipher(
12 | algorithms.AES(key),
13 | modes.CBC(key[:16]),
14 | backend=backend
15 | )
16 |
17 | def encrypt(self, plaintext):
18 | encryptor = self.cipher.encryptor()
19 | return encryptor.update(plaintext) + encryptor.finalize()
20 |
21 | def decrypt(self, ciphertext):
22 | decryptor = self.cipher.decryptor()
23 | return decryptor.update(ciphertext) + decryptor.finalize()
24 |
--------------------------------------------------------------------------------
/dingtalk/crypto/pkcs7.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | from dingtalk.core.utils import to_binary, byte2int
4 |
5 |
6 | class PKCS7Encoder(object):
7 | block_size = 32
8 |
9 | @classmethod
10 | def encode(cls, text):
11 | length = len(text)
12 | padding_count = cls.block_size - length % cls.block_size
13 | if padding_count == 0:
14 | padding_count = cls.block_size
15 | padding = to_binary(chr(padding_count))
16 | return text + padding * padding_count
17 |
18 | @classmethod
19 | def decode(cls, decrypted):
20 | padding = byte2int(decrypted[-1])
21 | if padding < 1 or padding > 32:
22 | padding = 0
23 | return decrypted[:-padding]
24 |
--------------------------------------------------------------------------------
/dingtalk/crypto/pycrypto.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | from Crypto.Cipher import AES
4 |
5 |
6 | class DingTalkCipher(object):
7 |
8 | def __init__(self, key):
9 | self.cipher = AES.new(key, AES.MODE_CBC, key[:16])
10 |
11 | def encrypt(self, plaintext):
12 | return self.cipher.encrypt(plaintext)
13 |
14 | def decrypt(self, ciphertext):
15 | return self.cipher.decrypt(ciphertext)
16 |
--------------------------------------------------------------------------------
/dingtalk/model/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
--------------------------------------------------------------------------------
/dingtalk/model/field.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import random
5 | import string
6 |
7 |
8 | class FieldBase(object):
9 | component_name = None
10 |
11 | def __init__(self, **kwargs):
12 | for k, v in kwargs.items():
13 | if callable(v):
14 | v = v()
15 | setattr(self, k, v)
16 | if getattr(kwargs, 'id', None) is None:
17 | self.id = self.gen_id()
18 |
19 | def gen_id(self):
20 | return "%s-%s" % (self.component_name, random.sample(string.ascii_uppercase + string.digits, 8))
21 |
22 | def get_dict(self):
23 | assert self.component_name
24 | return {'component_name': self.component_name, "props": self.get_data()}
25 |
26 | def get_data(self):
27 | ret = {}
28 | for k in dir(self):
29 | if k.startswith('_') or k == 'component_name':
30 | continue
31 | v = getattr(self, k, None)
32 | if v is None or hasattr(v, '__call__'):
33 | continue
34 | if v is not None:
35 | if isinstance(v, FieldBase):
36 | v = v.get_data()
37 | ret[k] = v
38 | return ret
39 |
40 |
41 | class TextField(FieldBase):
42 | component_name = "TextField"
43 |
44 | def __init__(self, label, required=True, placeholder='', **kwargs):
45 | """
46 |
47 | :param label: 表单组件名称
48 | :param required: 是否必填
49 | :param placeholder: 输入提示
50 | :param kwargs:
51 | """
52 | super(TextField, self).__init__(required=required, placeholder=placeholder, label=label, **kwargs)
53 |
54 |
55 | class TextareaField(FieldBase):
56 | component_name = "TextareaField"
57 |
58 | def __init__(self, label, required=True, placeholder='', **kwargs):
59 | """
60 |
61 | :param label: 表单组件名称
62 | :param required: 是否必填
63 | :param placeholder: 输入提示
64 | :param kwargs:
65 | """
66 | super(TextareaField, self).__init__(required=required, placeholder=placeholder, label=label, **kwargs)
67 |
68 |
69 | class MoneyField(FieldBase):
70 | component_name = "MoneyField"
71 |
72 | def __init__(self, label, required=True, placeholder='', not_upper='', **kwargs):
73 | """
74 |
75 | :param label: 表单组件名称
76 | :param required: 是否必填
77 | :param placeholder: 输入提示
78 | :param kwargs:
79 | """
80 | super(MoneyField, self).__init__(
81 | required=required, placeholder=placeholder, label=label, not_upper=not_upper, **kwargs
82 | )
83 |
84 |
85 | class NumberField(FieldBase):
86 | component_name = "NumberField"
87 |
88 | def __init__(self, label, required=True, placeholder='', unit='', **kwargs):
89 | """
90 |
91 | :param label: 表单组件名称
92 | :param required: 是否必填
93 | :param placeholder: 输入提示
94 | :param kwargs:
95 | """
96 | super(NumberField, self).__init__(required=required, placeholder=placeholder, label=label, unit=unit, **kwargs)
97 |
98 |
99 | class DDDateField(FieldBase):
100 | component_name = "DDDateField"
101 |
102 | def __init__(self, label, required=True, placeholder='', unit='', **kwargs):
103 | """
104 |
105 | :param label: 表单组件名称
106 | :param required: 是否必填
107 | :param placeholder: 输入提示
108 | :param kwargs:
109 | """
110 | super(DDDateField, self).__init__(required=required, placeholder=placeholder, label=label, unit=unit, **kwargs)
111 |
112 |
113 | class DDDateRangeField(FieldBase):
114 | component_name = "DDDateRangeField"
115 |
116 | def __init__(self, label, required=True, placeholder='', unit='', **kwargs):
117 | """
118 |
119 | :param label: 表单组件名称
120 | :param required: 是否必填
121 | :param placeholder: 输入提示
122 | :param kwargs:
123 | """
124 | super(DDDateRangeField, self).__init__(
125 | required=required, placeholder=placeholder, label=label, unit=unit, **kwargs
126 | )
127 |
--------------------------------------------------------------------------------
/dingtalk/model/message.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from dingtalk.core.utils import to_text
5 |
6 |
7 | class BodyBase(object):
8 |
9 | _msgtype = None
10 |
11 | def __init__(self, **kwargs):
12 | for k, v in kwargs.items():
13 | if callable(v):
14 | v = v()
15 | setattr(self, k, v)
16 |
17 | def get_dict(self):
18 | assert self._msgtype
19 | return {'msgtype': self._msgtype, self._msgtype: self.get_data()}
20 |
21 | def get_data(self):
22 | ret = {}
23 | for k in dir(self):
24 | if k.startswith('_'):
25 | continue
26 | v = getattr(self, k, None)
27 | if v is None or hasattr(v, '__call__'):
28 | continue
29 | if v is not None:
30 | if isinstance(v, BodyBase):
31 | v = v.get_data()
32 | ret[k] = v
33 | return ret
34 |
35 |
36 | class TextBody(BodyBase):
37 | _msgtype = 'text'
38 | content = None
39 |
40 | def __init__(self, content, **kwargs):
41 | """
42 | 文本消息
43 |
44 | :param content: 消息内容
45 | :param kwargs:
46 | """
47 | super(TextBody, self).__init__(content=content, **kwargs)
48 |
49 |
50 | class FileBody(BodyBase):
51 | _msgtype = 'file'
52 | media_id = None
53 |
54 | def __init__(self, media_id, **kwargs):
55 | """
56 | 文件消息
57 |
58 | :param media_id: 媒体文件id,可以调用上传媒体文件接口获取。10MB
59 | :param kwargs:
60 | """
61 | super(FileBody, self).__init__(media_id=media_id, **kwargs)
62 |
63 |
64 | class ImageBody(FileBody):
65 | _msgtype = 'image'
66 |
67 | def __init__(self, media_id, **kwargs):
68 | """
69 | 图片消息
70 |
71 | :param media_id: 图片媒体文件id,可以调用上传媒体文件接口获取。建议宽600像素 x 400像素,宽高比3:2
72 | :param kwargs:
73 | """
74 | super(FileBody, self).__init__(media_id=media_id, **kwargs)
75 |
76 |
77 | class VoiceBody(FileBody):
78 | _msgtype = 'voice'
79 | duration = None
80 |
81 | def __init__(self, media_id, duration=None, **kwargs):
82 | """
83 | 语音消息
84 |
85 | :param media_id: 语音媒体文件id,可以调用上传媒体文件接口获取。2MB,播放长度不超过60s,AMR格式
86 | :param duration: 正整数,小于60,表示音频时长
87 | :param kwargs:
88 | """
89 | super(VoiceBody, self).__init__(media_id=media_id, duration=duration, **kwargs)
90 |
91 |
92 | class LinkBody(BodyBase):
93 | _msgtype = 'link'
94 | messageUrl = None
95 | title = None
96 | text = None
97 | picUrl = None
98 |
99 | def __init__(self, message_url, pic_url, title, text, **kwargs):
100 | """
101 | 超链接消息
102 |
103 | :param message_url: 消息点击链接地址
104 | :param pic_url: 图片媒体文件id,可以调用上传媒体文件接口获取
105 | :param title: 消息标题
106 | :param text: 消息描述
107 | """
108 | super(LinkBody, self).__init__(messageUrl=message_url, picUrl=pic_url, title=title, text=text, **kwargs)
109 |
110 |
111 | class MarkdownBody(BodyBase):
112 | _msgtype = "markdown"
113 | title = None
114 | text = None
115 |
116 | def __init__(self, title, text, **kwargs):
117 | """
118 | markdown消息
119 |
120 | :param title: 首屏会话透出的展示内容
121 | :param text: markdown格式的消息
122 | :param kwargs:
123 | """
124 | super(MarkdownBody, self).__init__(title=title, text=text, **kwargs)
125 |
126 |
127 | class OaBodyContent(BodyBase):
128 | title = None
129 | _forms = None
130 | rich = None
131 | content = None
132 | image = None
133 | file_count = None
134 | author = None
135 |
136 | def __init__(self, title=None, content=None, author=None, image=None, file_count=None, forms=dict,
137 | rich_num=None, rish_unit=None, **kwargs):
138 | """
139 | OA消息 消息体
140 |
141 | :param title: 消息体的标题
142 | :param content: 消息体的内容,最多显示3行
143 | :param author: 自定义的作者名字
144 | :param image: 消息体中的图片media_id
145 | :param file_count: 自定义的附件数目。此数字仅供显示,钉钉不作验证
146 | :param forms: 消息体的表单
147 | :param rich_num: 单行富文本信息的数目
148 | :param rish_unit: 单行富文本信息的单位
149 | :param kwargs:
150 | """
151 | rich = None
152 | if rich_num is not None or rish_unit is not None:
153 | rich = {'num': rich_num, 'unit': rish_unit}
154 | super(OaBodyContent, self).__init__(title=title, content=content, author=author, image=image,
155 | file_count=file_count, _forms=forms, rich=rich, **kwargs)
156 |
157 | @property
158 | def form(self):
159 | if not self._forms:
160 | return None
161 | ret = []
162 | for k, v in self._forms.items():
163 | ret.append({"key": k, "value": v})
164 | return ret
165 |
166 |
167 | class OaBody(BodyBase):
168 | _msgtype = 'oa'
169 | message_url = None
170 | pc_message_url = None
171 | head = None
172 | body = None
173 |
174 | def __init__(self, message_url, head_bgcolor, head_text, body, pc_message_url=None, **kwargs):
175 | """
176 | OA消息
177 |
178 | :param message_url: 客户端点击消息时跳转到的H5地址
179 | :param head_bgcolor: 消息头部的背景颜色。长度限制为8个英文字符,其中前2为表示透明度,后6位表示颜色值。不要添加0x
180 | :param head_text: 消息的头部标题(向普通会话发送时有效,向企业会话发送时会被替换为微应用的名字),长度限制为最多10个字符
181 | :param body: OaBodyContent OA消息 消息体
182 | :param pc_message_url: PC端点击消息时跳转到的H5地址
183 | :param kwargs:
184 | """
185 | super(OaBody, self).__init__(message_url=message_url, head={"bgcolor": head_bgcolor, "text": head_text},
186 | body=body, pc_message_url=pc_message_url, **kwargs)
187 |
188 |
189 | class ActionCardBody(BodyBase):
190 | _msgtype = 'action_card'
191 | title = None
192 | markdown = None
193 |
194 | def __init__(self, title, markdown, **kwargs):
195 | super(ActionCardBody, self).__init__(title=title, markdown=markdown, **kwargs)
196 |
197 |
198 | class SingleActionCardBody(ActionCardBody):
199 | single_title = None
200 | single_url = None
201 |
202 | def __init__(self, title, markdown, single_title, single_url, **kwargs):
203 | """
204 | 整体跳转ActionCard消息
205 |
206 | :param title: 透出到会话列表和通知的文案
207 | :param markdown: 消息内容,支持markdown
208 | :param single_title: 标题
209 | :param single_url: 链接url
210 | :param kwargs:
211 | """
212 | super(SingleActionCardBody, self).__init__(title=title, markdown=markdown,
213 | single_title=single_title, single_url=single_url, **kwargs)
214 |
215 |
216 | class BtnActionCardBody(ActionCardBody):
217 | btn_orientation = None
218 | btn_json_list = None
219 |
220 | def __init__(self, title, markdown, btn_orientation, btn_list=(), **kwargs):
221 | """
222 | 独立跳转ActionCard消息
223 |
224 | :param title: 透出到会话列表和通知的文案
225 | :param markdown: 消息内容,支持markdown
226 | :param btn_orientation: 按钮排列方式,竖直排列(0),横向排列(1)
227 | :param btn_json_list: 按钮列表
228 | :param kwargs:
229 | """
230 | btn_orientation = to_text(btn_orientation)
231 | assert btn_orientation in ('0', '1')
232 | super(BtnActionCardBody, self).__init__(title=title, markdown=markdown,
233 | btn_orientation=btn_orientation, btn_json_list=list(btn_list), **kwargs)
234 |
235 | def add_btn(self, title, action_url):
236 | """
237 | 添加按钮
238 |
239 | :param title: 标题
240 | :param action_url: 链接url
241 | :return:
242 | """
243 | assert isinstance(self.btn_json_list, list)
244 | self.btn_json_list.append({'title': title, 'action_url': action_url})
245 |
--------------------------------------------------------------------------------
/dingtalk/storage/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 |
5 | class BaseStorage(object):
6 |
7 | def get(self, key, default=None):
8 | raise NotImplementedError()
9 |
10 | def set(self, key, value, ttl=None):
11 | raise NotImplementedError()
12 |
13 | def delete(self, key):
14 | raise NotImplementedError()
15 |
16 | def __getitem__(self, key):
17 | self.get(key)
18 |
19 | def __setitem__(self, key, value):
20 | self.set(key, value)
21 |
22 | def __delitem__(self, key):
23 | self.delete(key)
24 |
--------------------------------------------------------------------------------
/dingtalk/storage/cache.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import inspect
5 |
6 | from dingtalk.storage import BaseStorage
7 |
8 |
9 | def _is_cache_item(obj):
10 | return isinstance(obj, DingTalkCacheItem)
11 |
12 |
13 | class DingTalkCacheItem(object):
14 |
15 | def __init__(self, cache=None, name=None):
16 | self.cache = cache
17 | self.name = name
18 |
19 | def key_name(self, key):
20 | if isinstance(key, (tuple, list)):
21 | key = ':'.join(key)
22 |
23 | k = '{0}:{1}'.format(self.cache.prefix, self.name)
24 | if key is not None:
25 | k = '{0}:{1}'.format(k, key)
26 | return k
27 |
28 | def get(self, key=None, default=None):
29 | return self.cache.storage.get(self.key_name(key), default)
30 |
31 | def set(self, key=None, value=None, ttl=None):
32 | return self.cache.storage.set(self.key_name(key), value, ttl)
33 |
34 | def delete(self, key=None):
35 | return self.cache.storage.delete(self.key_name(key))
36 |
37 |
38 | class BaseCache(object):
39 |
40 | def __new__(cls, *args, **kwargs):
41 | self = super(BaseCache, cls).__new__(cls)
42 | api_endpoints = inspect.getmembers(self, _is_cache_item)
43 | for name, api in api_endpoints:
44 | api_cls = type(api)
45 | api = api_cls(self, name)
46 | setattr(self, name, api)
47 | return self
48 |
49 | def __init__(self, storage, prefix='client'):
50 | assert isinstance(storage, BaseStorage)
51 | self.storage = storage
52 | self.prefix = prefix
53 |
54 |
55 | class DingTalkCache(BaseCache):
56 | access_token = DingTalkCacheItem()
57 | jsapi_ticket = DingTalkCacheItem()
58 |
59 |
60 | class ChannelCache(BaseCache):
61 | channel_token = DingTalkCacheItem()
62 | jsapi_ticket = DingTalkCacheItem()
63 |
64 |
65 | class ISVCache(BaseCache):
66 | suite_ticket = DingTalkCacheItem()
67 | suite_access_token = DingTalkCacheItem()
68 | permanent_code = DingTalkCacheItem()
69 | ch_permanent_code = DingTalkCacheItem()
70 |
--------------------------------------------------------------------------------
/dingtalk/storage/kvstorage.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import json
5 |
6 | from dingtalk.core.utils import to_text
7 | from dingtalk.storage import BaseStorage
8 |
9 |
10 | class KvStorage(BaseStorage):
11 |
12 | def __init__(self, kvdb, prefix='dingtalk'):
13 | for method_name in ('get', 'set', 'delete'):
14 | assert hasattr(kvdb, method_name)
15 | self.kvdb = kvdb
16 | self.prefix = prefix
17 |
18 | def key_name(self, key):
19 | return '{0}:{1}'.format(self.prefix, key)
20 |
21 | def get(self, key, default=None):
22 | key = self.key_name(key)
23 | value = self.kvdb.get(key)
24 | if value is None:
25 | return default
26 | return json.loads(to_text(value))
27 |
28 | def set(self, key, value, ttl=None):
29 | if value is None:
30 | return
31 | key = self.key_name(key)
32 | value = json.dumps(value)
33 | self.kvdb.set(key, value, ttl)
34 |
35 | def delete(self, key):
36 | key = self.key_name(key)
37 | self.kvdb.delete(key)
38 |
--------------------------------------------------------------------------------
/dingtalk/storage/memorystorage.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import time
5 |
6 | from dingtalk.storage import BaseStorage
7 |
8 |
9 | class MemoryStorage(BaseStorage):
10 |
11 | def __init__(self):
12 | self._data = {}
13 |
14 | def get(self, key, default=None):
15 | ret = self._data.get(key, None)
16 | if ret is None or len(ret) != 2:
17 | return default
18 | else:
19 | value = ret[0]
20 | expires_at = ret[1]
21 | if expires_at is None or expires_at > time.time():
22 | return value
23 | else:
24 | return default
25 |
26 | def set(self, key, value, ttl=None):
27 | if value is None:
28 | return
29 | self._data[key] = (value, int(time.time()) + ttl)
30 |
31 | def delete(self, key):
32 | self._data.pop(key, None)
33 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = dingtalk-sdk
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | ================
3 |
4 | Version 1.3.3
5 | ------------------
6 | + fix 创建审批bug
7 |
8 | Version 1.3.2
9 | ------------------
10 | + 增加审批待办接口
11 |
12 | Version 1.3.1
13 | ------------------
14 |
15 | + 增加DING日程相关接口
16 | + 增加钉钉运动相关接口
17 | + 增加公告相关接口
18 | + 增加待办事项相关接口
19 | + 增加撤回工作通知消息接口
20 | + fix 发送工作通知参数错误
21 |
22 | Version 1.3.0
23 | ------------------
24 |
25 | + 增加企业外部联系人相关接口
26 | + 增加日志相关接口
27 | + 更新淘宝接口
28 |
29 | Version 1.2.8
30 | ------------------
31 |
32 | + 发起审批实例支持会签/或签
33 |
34 | Version 1.2.7
35 | ------------------
36 |
37 | + fix 会话 获取群会话请求方法错误
38 |
39 | Version 1.2.6
40 | ------------------
41 |
42 | + 增加asyncsend_v2接口
43 |
44 | Version 1.2.4
45 | ------------------
46 |
47 | + 增加新接口
48 |
49 | Version 1.2.3
50 | ------------------
51 |
52 | + top接口成功状态判断增加 is_success, error_response 增加msg错误信息判断
53 |
54 | Version 1.2.2
55 | ------------------
56 |
57 | + 增加淘宝接口
58 |
59 | Version 1.1.9
60 | ------------------
61 |
62 | + fix callback接口的返回失败接口 使用错误
63 |
64 |
65 | Version 1.1.8
66 | ------------------
67 |
68 | + ssl._create_unverified_context 增加异常判断
69 |
70 |
71 | Version 1.1.7
72 | ------------------
73 |
74 | + 新增app_key app_secret 获取 access_token 的 AppKeyClient
75 |
76 | Version 1.1.5
77 | ------------------
78 |
79 | + [fix]the list_message_status api 's method is post
80 |
81 | Version 1.1.4
82 | ------------------
83 |
84 | + 解决接口返回 ObjectDict 时 json_loads 未return bug
85 |
86 | Version 1.1.3
87 | ------------------
88 |
89 | + 接口返回结果使用 ObjectDict
90 |
91 | Version 1.1.2
92 | ------------------
93 |
94 | + 增加 智能人事接口
95 |
96 | Version 1.1.1
97 | ------------------
98 |
99 | + 修复 LinkBody 参数层级错误
100 | + 修复 OaBodyContent form 参数处理错误
101 | + 增加 testcase
102 | + 增加 文档
103 |
104 |
105 | Version 1.1.0
106 | ------------------
107 |
108 | + 企业钉钉接口
109 | + ISV服务商接口
110 |
111 |
--------------------------------------------------------------------------------
/docs/client/api/attendance.rst:
--------------------------------------------------------------------------------
1 | 考勤接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Attendance
7 | :members:
8 | :inherited-members:
9 |
10 |
--------------------------------------------------------------------------------
/docs/client/api/blackboard.rst:
--------------------------------------------------------------------------------
1 | 公告接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: BlackBoard
7 | :members:
8 | :inherited-members:
9 |
10 |
--------------------------------------------------------------------------------
/docs/client/api/bpms.rst:
--------------------------------------------------------------------------------
1 | 审批流接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Bpms
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/calendar.rst:
--------------------------------------------------------------------------------
1 | 日程接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Calendar
7 | :members:
8 | :inherited-members:
9 |
10 |
--------------------------------------------------------------------------------
/docs/client/api/callback.rst:
--------------------------------------------------------------------------------
1 | 回调接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Callback
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/chat.rst:
--------------------------------------------------------------------------------
1 | 会话接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Chat
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/checkin.rst:
--------------------------------------------------------------------------------
1 | 签到接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Checkin
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/cspace.rst:
--------------------------------------------------------------------------------
1 | 钉盘接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Cspace
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/department.rst:
--------------------------------------------------------------------------------
1 | 部门接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Department
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/employeerm.rst:
--------------------------------------------------------------------------------
1 | 智能人事
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Employeerm
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/ext.rst:
--------------------------------------------------------------------------------
1 | 外部联系人接口(废弃)
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Ext
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/extcontact.rst:
--------------------------------------------------------------------------------
1 | 外部联系人接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: ExtContact
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/health.rst:
--------------------------------------------------------------------------------
1 | 健康接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Health
7 | :members:
8 | :inherited-members:
9 |
10 |
--------------------------------------------------------------------------------
/docs/client/api/message.rst:
--------------------------------------------------------------------------------
1 | 消息接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Message
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/microapp.rst:
--------------------------------------------------------------------------------
1 | 微应用接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: MicroApp
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/report.rst:
--------------------------------------------------------------------------------
1 | 日志接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Report
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/role.rst:
--------------------------------------------------------------------------------
1 | 角色接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: Role
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/taobao.rst:
--------------------------------------------------------------------------------
1 | 淘宝接口
2 | ===================
3 |
4 | .. automodule:: dingtalk.client.api.taobao
5 | :members:
6 | :inherited-members:
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/client/api/user.rst:
--------------------------------------------------------------------------------
1 | 用户接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: User
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/api/workrecord.rst:
--------------------------------------------------------------------------------
1 | 待办接口
2 | ===================
3 |
4 | .. module:: dingtalk.client.api
5 |
6 | .. autoclass:: WorkRecord
7 | :members:
8 | :inherited-members:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/client/index.rst:
--------------------------------------------------------------------------------
1 | 钉钉企业内部开发接口
2 | ===========================================
3 |
4 | .. module:: dingtalk.client
5 |
6 | .. autoclass:: DingTalkClient
7 | :members:
8 | :inherited-members:
9 |
10 | `DingTalkClient` 基本使用方法::
11 |
12 | from dingtalk import SecretClient, AppKeyClient
13 |
14 | client = SecretClient('corp_id', 'secret') # 旧 access_token 获取方式
15 | client = AppKeyClient('corp_id', 'app_key', 'app_secret') # 新 access_token 获取方式
16 |
17 | user = client.user.get('userid')
18 | departments = client.department.list()
19 | # 以此类推,参见下面的 API 说明
20 | # client.chat.xxx()
21 | # client.role.xxx()
22 |
23 | 如果不提供 ``storage`` 参数,默认使用 ``dingtalk.storage.memorystorage.MemoryStorage`` 类型,
24 | 注意该类型不是线程安全的,而且非持久化保存,不推荐生产环境使用。
25 |
26 | .. toctree::
27 | :maxdepth: 2
28 | :glob:
29 |
30 | api/*
31 |
32 |
--------------------------------------------------------------------------------
/docs/client/isv.rst:
--------------------------------------------------------------------------------
1 | 钉钉应用服务商(ISV)接口
2 | ===========================================
3 |
4 | .. module:: dingtalk.client.isv
5 |
6 | .. autoclass:: ISVClient
7 | :members:
8 | :inherited-members:
9 |
10 | `ISVClient` 基本使用方法::
11 |
12 | from dingtalk import ISVClient
13 |
14 | client = ISVClient('corp_id', 'secret', 'token', 'aes_key')
15 | corp_info = client.get_auth_info('corpid')
16 |
17 | corp_client = client.get_dingtalk_client('corpid')
18 | user = corp_client.user.get('userid')
19 | departments = corp_client.department.list()
20 | # 以此类推,corp_client可针对企业执行api
21 |
22 | .. toctree::
23 | :maxdepth: 1
24 | :glob:
25 |
26 | index
27 |
28 |
29 | 如果不提供 ``storage`` 参数,默认使用 ``dingtalk.storage.memorystorage.MemoryStorage`` 类型,
30 | 注意该类型不是线程安全的,而且非持久化保存,不推荐生产环境使用。
31 |
32 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/master/config
8 |
9 | import sphinx_rtd_theme
10 |
11 | # -- Path setup --------------------------------------------------------------
12 |
13 | # If extensions (or modules to document with autodoc) are in another directory,
14 | # add these directories to sys.path here. If the directory is relative to the
15 | # documentation root, use os.path.abspath to make it absolute, like shown here.
16 | #
17 | import os
18 | import sys
19 | import dingtalk
20 |
21 |
22 | sys.path.insert(0, os.path.abspath('..'))
23 |
24 | # -- Project information -----------------------------------------------------
25 |
26 | project = 'dingtalk-sdk'
27 | copyright = '2018, 007gzs'
28 | author = '007gzs'
29 |
30 | # The short X.Y version
31 | version = dingtalk.__version__
32 | # The full version, including alpha/beta/rc tags
33 | release = dingtalk.__version__
34 |
35 |
36 | # -- General configuration ---------------------------------------------------
37 |
38 | # If your documentation needs a minimal Sphinx version, state it here.
39 | #
40 | # needs_sphinx = '1.0'
41 |
42 | # Add any Sphinx extension module names here, as strings. They can be
43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
44 | # ones.
45 | extensions = [
46 | 'sphinx.ext.autodoc',
47 | 'sphinx.ext.doctest',
48 | 'sphinx.ext.intersphinx',
49 | 'sphinx.ext.todo',
50 | 'sphinx.ext.coverage',
51 | 'sphinx.ext.mathjax',
52 | 'sphinx.ext.ifconfig',
53 | 'sphinx.ext.viewcode',
54 | 'sphinx.ext.githubpages',
55 | ]
56 |
57 | # Add any paths that contain templates here, relative to this directory.
58 | templates_path = ['_templates']
59 |
60 | # The suffix(es) of source filenames.
61 | # You can specify multiple suffix as a list of string:
62 | #
63 | # source_suffix = ['.rst', '.md']
64 | source_suffix = '.rst'
65 |
66 | # The master toctree document.
67 | master_doc = 'index'
68 |
69 | # The language for content autogenerated by Sphinx. Refer to documentation
70 | # for a list of supported languages.
71 | #
72 | # This is also used if you do content translation via gettext catalogs.
73 | # Usually you set "language" from the command line for these cases.
74 | language = 'zh_cn'
75 |
76 | # List of patterns, relative to source directory, that match files and
77 | # directories to ignore when looking for source files.
78 | # This pattern also affects html_static_path and html_extra_path .
79 | exclude_patterns = []
80 |
81 | # The name of the Pygments (syntax highlighting) style to use.
82 | pygments_style = 'sphinx'
83 |
84 |
85 | # -- Options for HTML output -------------------------------------------------
86 |
87 | # The theme to use for HTML and HTML Help pages. See the documentation for
88 | # a list of builtin themes.
89 | #
90 | # html_theme = 'alabaster'
91 |
92 | html_theme = "sphinx_rtd_theme"
93 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
94 |
95 | # Theme options are theme-specific and customize the look and feel of a theme
96 | # further. For a list of options available for each theme, see the
97 | # documentation.
98 | #
99 | # html_theme_options = {}
100 |
101 | # Add any paths that contain custom static files (such as style sheets) here,
102 | # relative to this directory. They are copied after the builtin static files,
103 | # so a file named "default.css" will overwrite the builtin "default.css".
104 | html_static_path = ['_static']
105 |
106 | # Custom sidebar templates, must be a dictionary that maps document names
107 | # to template names.
108 | #
109 | # The default sidebars (for documents that don't match any pattern) are
110 | # defined by theme itself. Builtin themes are using these templates by
111 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
112 | # 'searchbox.html']``.
113 | #
114 | # html_sidebars = {}
115 |
116 |
117 | # -- Options for HTMLHelp output ---------------------------------------------
118 |
119 | # Output file base name for HTML help builder.
120 | htmlhelp_basename = 'dingtalk-sdkdoc'
121 |
122 |
123 | # -- Options for LaTeX output ------------------------------------------------
124 |
125 | latex_elements = {
126 | # The paper size ('letterpaper' or 'a4paper').
127 | #
128 | # 'papersize': 'letterpaper',
129 |
130 | # The font size ('10pt', '11pt' or '12pt').
131 | #
132 | # 'pointsize': '10pt',
133 |
134 | # Additional stuff for the LaTeX preamble.
135 | #
136 | # 'preamble': '',
137 |
138 | # Latex figure (float) alignment
139 | #
140 | # 'figure_align': 'htbp',
141 | }
142 |
143 | # Grouping the document tree into LaTeX files. List of tuples
144 | # (source start file, target name, title,
145 | # author, documentclass [howto, manual, or own class]).
146 | latex_documents = [
147 | (master_doc, 'dingtalk-sdk.tex', 'dingtalk-sdk Documentation',
148 | '007gzs', 'manual'),
149 | ]
150 |
151 |
152 | # -- Options for manual page output ------------------------------------------
153 |
154 | # One entry per manual page. List of tuples
155 | # (source start file, name, description, authors, manual section).
156 | man_pages = [
157 | (master_doc, 'dingtalk-sdk', 'dingtalk-sdk Documentation',
158 | [author], 1)
159 | ]
160 |
161 |
162 | # -- Options for Texinfo output ----------------------------------------------
163 |
164 | # Grouping the document tree into Texinfo files. List of tuples
165 | # (source start file, target name, title, author,
166 | # dir menu entry, description, category)
167 | texinfo_documents = [
168 | (master_doc, 'dingtalk-sdk', 'dingtalk-sdk Documentation',
169 | author, 'dingtalk-sdk', 'One line description of project.',
170 | 'Miscellaneous'),
171 | ]
172 |
173 |
174 | # -- Extension configuration -------------------------------------------------
175 |
176 | # -- Options for intersphinx extension ---------------------------------------
177 |
178 | # Example configuration for intersphinx: refer to the Python standard library.
179 | intersphinx_mapping = {'https://docs.python.org/': None}
180 |
181 | # -- Options for todo extension ----------------------------------------------
182 |
183 | # If true, `todo` and `todoList` produce output, else they produce nothing.
184 | todo_include_todos = True
185 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. dingtalk-sdk documentation master file, created by
2 | sphinx-quickstart on Fri May 4 11:18:22 2018.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | dingtalk-sdk 使用文档
7 | ========================================
8 |
9 | dingtalk-sdk 是一个钉钉开放平台的第三方 Python SDK, 实现了 企业内部开发 和 应用服务商(ISV)的 API。
10 |
11 | 快速入门
12 | -------------
13 |
14 | .. toctree::
15 | :maxdepth: 2
16 |
17 | install
18 |
19 |
20 | 企业内部开发
21 | --------------------
22 | 建议在使用前先阅读 `钉钉开放平台文档 `_
23 |
24 | .. toctree::
25 | :glob:
26 | :maxdepth: 2
27 |
28 | model/message
29 | model/field
30 | client/index
31 |
32 | 应用服务商(ISV)
33 | ----------------------------
34 | .. toctree::
35 | :glob:
36 | :maxdepth: 2
37 |
38 | client/isv
39 |
40 | 未实现接口
41 | --------------------
42 | 由于钉钉接口过多,文档较分散,有未实现的接口可以提交 Issues, sdk未更新时候可根据下面代码临时使用
43 | post/get接口中的access_token,top接口中的session会在请求时自动设置,无需手动添加
44 |
45 | .. module:: dingtalk.client.base
46 |
47 | .. autoclass:: BaseClient
48 |
49 | .. automethod:: get
50 | .. automethod:: post
51 | .. automethod:: top_request
52 |
53 | 调用示例::
54 |
55 | client = SecretClient('CORP_ID', 'CORP_SECRET')
56 |
57 | # top 接口: 获取考勤组列表详情
58 | ret = client._top_request(
59 | 'dingtalk.smartwork.attends.getsimplegroups',
60 | {
61 | "offset": 0,
62 | "size": 10
63 | }
64 | )
65 | has_more = ret.result.has_more
66 | groups = ret.result.groups
67 |
68 |
69 | # get 接口:获取子部门ID列表
70 | ret = client.get(
71 | '/department/list_ids',
72 | {'id': 0}
73 | )
74 | sub_dept_id_list = ret.sub_dept_id_list
75 |
76 |
77 | # post 接口:创建会话
78 | return self._post(
79 | '/chat/create',
80 | {
81 | 'name': "群名称",
82 | 'owner': "zhangsan",
83 | 'useridlist': ["zhangsan", "lisi"]
84 | }
85 | )
86 | chatid = ret.chatid
87 |
88 |
89 | 示例项目
90 | ---------------------
91 |
92 | `django demo `_
93 |
94 |
95 | Changelogs
96 | ---------------
97 |
98 | .. toctree::
99 | :maxdepth: 1
100 |
101 | changelog
102 |
103 |
--------------------------------------------------------------------------------
/docs/install.rst:
--------------------------------------------------------------------------------
1 | 安装与升级
2 | ==========
3 |
4 | 目前 dingtalk-sdk 支持的 Python 环境有 2.7, 3.4, 3.5, 3.6 和 pypy。
5 |
6 | dingtalk-sdk 消息加解密同时兼容 cryptography 和 PyCrypto, 优先使用 cryptography 库。
7 | 可先自行安装 cryptography 或者 PyCrypto 库::
8 |
9 | # 安装 cryptography
10 | pip install cryptography>=0.8.2
11 | # 或者安装 PyCrypto
12 | pip install pycrypto>=2.6.1
13 |
14 | 为了简化安装过程,推荐使用 pip 进行安装
15 |
16 | .. code-block:: bash
17 |
18 | pip install dingtalk-sdk
19 | # with cryptography
20 | pip install dingtalk-sdk[cryptography]
21 | # with pycrypto
22 | pip install dingtalk-sdk[pycrypto]
23 |
24 | 升级 dingtalk-sdk 到新版本::
25 |
26 | pip install -U dingtalk-sdk
27 |
28 | 如果需要安装 GitHub 上的最新代码::
29 |
30 | pip install https://github.com/007gzs/dingtalk-sdk/archive/master.zip
31 |
32 |
--------------------------------------------------------------------------------
/docs/model/field.rst:
--------------------------------------------------------------------------------
1 | 表单组件
2 | ===================
3 |
4 | .. module:: dingtalk.model.field
5 |
6 | .. autoclass:: TextField
7 | :members:
8 | :inherited-members:
9 |
10 | .. automethod:: __init__
11 |
12 | .. autoclass:: TextareaField
13 | :members:
14 | :inherited-members:
15 |
16 | .. automethod:: __init__
17 |
18 | .. autoclass:: MoneyField
19 | :members:
20 | :inherited-members:
21 |
22 | .. automethod:: __init__
23 |
24 | .. autoclass:: NumberField
25 | :members:
26 | :inherited-members:
27 |
28 | .. automethod:: __init__
29 |
30 | .. autoclass:: DDDateField
31 | :members:
32 | :inherited-members:
33 |
34 | .. automethod:: __init__
35 |
36 | .. autoclass:: DDDateRangeField
37 | :members:
38 | :inherited-members:
39 |
40 | .. automethod:: __init__
41 |
42 |
--------------------------------------------------------------------------------
/docs/model/message.rst:
--------------------------------------------------------------------------------
1 | 消息实体
2 | ===================
3 |
4 | .. module:: dingtalk.model.message
5 |
6 | .. autoclass:: TextBody
7 | :members:
8 | :inherited-members:
9 |
10 | .. automethod:: __init__
11 |
12 | .. autoclass:: FileBody
13 | :members:
14 | :inherited-members:
15 |
16 | .. automethod:: __init__
17 |
18 | .. autoclass:: ImageBody
19 | :members:
20 | :inherited-members:
21 |
22 | .. automethod:: __init__
23 |
24 | .. autoclass:: VoiceBody
25 | :members:
26 | :inherited-members:
27 |
28 | .. automethod:: __init__
29 |
30 | .. autoclass:: LinkBody
31 | :members:
32 | :inherited-members:
33 |
34 | .. automethod:: __init__
35 |
36 | .. autoclass:: MarkdownBody
37 | :members:
38 | :inherited-members:
39 |
40 | .. automethod:: __init__
41 |
42 | .. autoclass:: OaBodyContent
43 | :members:
44 | :inherited-members:
45 |
46 | .. automethod:: __init__
47 |
48 | .. autoclass:: OaBody
49 | :members:
50 | :inherited-members:
51 |
52 | .. automethod:: __init__
53 |
54 | .. autoclass:: SingleActionCardBody
55 | :members:
56 | :inherited-members:
57 |
58 | .. automethod:: __init__
59 |
60 | .. autoclass:: BtnActionCardBody
61 | :members:
62 | :inherited-members:
63 |
64 | .. automethod:: __init__
65 |
66 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests
3 |
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | enum34>=1.1.4; python_version < '3.4'
2 | six>=1.8.0
3 | requests>=2.4.3
4 | optionaldict>=0.1.0
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude = .svn,CVS,.bzr,.hg,.git,__pycache,.ropeproject
3 | max-line-length = 120
4 | per-file-ignores =
5 | dingtalk/client/api/taobao.py:E501,W605
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # encoding: utf-8
3 |
4 |
5 | """A setuptools based setup module.
6 |
7 | See:
8 | https://packaging.python.org/en/latest/distributing.html
9 | https://github.com/pypa/sampleproject
10 | """
11 |
12 | # Always prefer setuptools over distutils
13 | from setuptools import setup
14 | from setuptools.command.test import test as TestCommand
15 | # To use a consistent encoding
16 | from codecs import open
17 | from os import path
18 | import sys
19 |
20 | import ssl
21 | try:
22 | ssl._create_default_https_context = ssl._create_unverified_context
23 | except Exception:
24 | pass
25 |
26 | here = path.abspath(path.dirname(__file__))
27 |
28 |
29 | class PyTest(TestCommand):
30 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
31 |
32 | def initialize_options(self):
33 | TestCommand.initialize_options(self)
34 | self.pytest_args = []
35 |
36 | def finalize_options(self):
37 | TestCommand.finalize_options(self)
38 | self.test_args = []
39 | self.test_suite = True
40 |
41 | def run_tests(self):
42 | import pytest
43 | errno = pytest.main(self.pytest_args)
44 | sys.exit(errno)
45 |
46 |
47 | cmdclass = {}
48 | cmdclass['test'] = PyTest
49 |
50 | # Get the long description from the README file
51 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
52 | long_description = f.read()
53 |
54 | with open('requirements.txt') as f:
55 | requirements = [line for line in f.read().splitlines() if line]
56 |
57 | setup(
58 | name='dingtalk-sdk',
59 | version='1.3.8',
60 | keywords='dingding, ding, dtalk, dingtalk, SDK',
61 | description='DingTalk SDK for Python',
62 | long_description=long_description,
63 | url='https://github.com/007gzs/dingtalk-sdk',
64 | author='007gzs',
65 | author_email='007gzs@sina.com',
66 | license='LGPL v3',
67 | classifiers=[
68 | 'Development Status :: 3 - Alpha',
69 | 'Intended Audience :: Developers',
70 | 'Topic :: Software Development :: Build Tools',
71 | 'License :: OSI Approved :: '
72 | 'GNU Lesser General Public License v3 (LGPLv3)',
73 | 'Programming Language :: Python :: 2',
74 | 'Programming Language :: Python :: 2.7',
75 | 'Programming Language :: Python :: 3',
76 | 'Programming Language :: Python :: 3.3',
77 | 'Programming Language :: Python :: 3.4',
78 | 'Programming Language :: Python :: 3.5',
79 | 'Programming Language :: Python :: 3.6',
80 | ],
81 | packages=[
82 | 'dingtalk',
83 | 'dingtalk.core',
84 | 'dingtalk.crypto',
85 | 'dingtalk.storage',
86 | 'dingtalk.model',
87 | 'dingtalk.client',
88 | 'dingtalk.client.api'
89 | ],
90 | install_requires=requirements,
91 | zip_safe=False,
92 | include_package_data=True,
93 | tests_require=[
94 | 'pytest',
95 | 'redis',
96 | 'pymemcache',
97 | ],
98 | cmdclass=cmdclass,
99 | extras_require={
100 | 'cryptography': ['cryptography'],
101 | 'pycrypto': ['pycrypto'],
102 | },
103 | )
104 |
--------------------------------------------------------------------------------
/tests/test_crypto.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | import unittest
4 |
5 | import json
6 |
7 | from dingtalk import crypto as _crypto
8 | from dingtalk.crypto import DingTalkCrypto
9 |
10 |
11 | class PrpCryptoMock(_crypto.PrpCrypto):
12 |
13 | def get_random_string(self):
14 | return '1234567890123456'
15 |
16 |
17 | class CryptoTestCase(unittest.TestCase):
18 |
19 | token = '123456'
20 | encoding_aes_key = '4g5j64qlyl3zvetqxz5jiocdr586fn2zvjpa8zls3ij'
21 | suite_key = 'suite4xxxxxxxxxxxxxxx'
22 |
23 | def test_encrypt_message(self):
24 | origin_crypto = _crypto.PrpCrypto
25 | _crypto.PrpCrypto = PrpCryptoMock
26 |
27 | nonce = 'nEXhMP4r'
28 | timestamp = '1445827045067'
29 | msg = """{"EventType":"check_create_suite_url","Random":"LPIdSnlF","TestSuiteKey":"suite4xxxxxxxxxxxxxxx"}"""
30 |
31 | expected = {
32 | 'msg_signature': 'bcf6dcefa4ce2dbaf7b0666c7264d46fd9aad4bd',
33 | 'encrypt': '5DJFWzjRNOQk+5GSZxW+VrFMDWCIidPjEjg3//gm5x556BedVi62rDj1F9uXU97a4jw1R4FACUv9RCpoDobNqxhxRB2Yt'
34 | 'W901k4KHbP1/wpFJ3xdLG0n0A8U1VhENg80zKJd+YROR0YMGum4WYuoXJ6J98vt0ihYeIFoapNddLML5MyNAGM9saSpko'
35 | 'uDMSvD+iU14i7V8ix1ia1Tb9ogog==',
36 | 'timeStamp': '1445827045067',
37 | 'nonce': 'nEXhMP4r'
38 | }
39 | crypto = DingTalkCrypto(self.token, self.encoding_aes_key, self.suite_key)
40 | encrypted = crypto.encrypt_message(msg, nonce, timestamp)
41 |
42 | _crypto.PrpCrypto = origin_crypto
43 |
44 | self.assertEqual(expected.keys(), encrypted.keys())
45 | for key in expected.keys():
46 | self.assertEqual(expected[key], encrypted[key])
47 |
48 | def test_decrypt_message(self):
49 | from dingtalk.core.utils import to_text
50 | jsonstr = '{"encrypt":"1a3NBxmCFwkCJvfoQ7WhJHB+iX3qHPsc9JbaDznE1i03peOk1LaOQoRz3+nlyGNhwmwJ3vDMG+OzrHMeiZI7g' \
51 | 'TRWVdUBmfxjZ8Ej23JVYa9VrYeJ5as7XM/ZpulX8NEQis44w53h1qAgnC3PRzM7Zc/D6Ibr0rgUathB6zRHP8PYrfgnNOS9Ph' \
52 | 'SBdHlegK+AGGanfwjXuQ9+0pZcy0w9lQ=="}'
53 |
54 | signature = '5a65ceeef9aab2d149439f82dc191dd6c5cbe2c0'
55 | timestamp = '1445827045067'
56 | nonce = 'nEXhMP4r'
57 |
58 | crypto = DingTalkCrypto(self.token, self.encoding_aes_key, self.suite_key)
59 | msg = crypto.decrypt_message(jsonstr, signature, timestamp, nonce)
60 | msg_dict = json.loads(to_text(msg))
61 | self.assertEqual('check_create_suite_url', msg_dict['EventType'])
62 | self.assertEqual('LPIdSnlF', msg_dict['Random'])
63 | self.assertEqual(self.suite_key, msg_dict['TestSuiteKey'])
64 |
65 | def test_decrypt_binary_message(self):
66 | from dingtalk.core.utils import to_text
67 | jsonbinary = b'{"encrypt":"1a3NBxmCFwkCJvfoQ7WhJHB+iX3qHPsc9JbaDznE1i03peOk1LaOQoRz3+nlyGNhwmwJ3vDMG+OzrHMei' \
68 | b'ZI7gTRWVdUBmfxjZ8Ej23JVYa9VrYeJ5as7XM/ZpulX8NEQis44w53h1qAgnC3PRzM7Zc/D6Ibr0rgUathB6zRHP8PYrf' \
69 | b'gnNOS9PhSBdHlegK+AGGanfwjXuQ9+0pZcy0w9lQ=="}'
70 |
71 | signature = '5a65ceeef9aab2d149439f82dc191dd6c5cbe2c0'
72 | timestamp = '1445827045067'
73 | nonce = 'nEXhMP4r'
74 |
75 | crypto = DingTalkCrypto(self.token, self.encoding_aes_key, self.suite_key)
76 | msg = crypto.decrypt_message(jsonbinary, signature, timestamp, nonce)
77 | msg_dict = json.loads(to_text(msg))
78 | self.assertEqual('check_create_suite_url', msg_dict['EventType'])
79 | self.assertEqual('LPIdSnlF', msg_dict['Random'])
80 | self.assertEqual(self.suite_key, msg_dict['TestSuiteKey'])
81 |
82 | def test_decrypt_encrypt_str(self):
83 | from dingtalk.core.utils import to_text
84 | signature = '5a65ceeef9aab2d149439f82dc191dd6c5cbe2c0'
85 | timestamp = '1445827045067'
86 | nonce = 'nEXhMP4r'
87 | encrypt_str = '1a3NBxmCFwkCJvfoQ7WhJHB+iX3qHPsc9JbaDznE1i03peOk1LaOQoRz3+nlyGNhwmwJ3vDMG+OzrHMeiZI7gTRWVdUBm' \
88 | 'fxjZ8Ej23JVYa9VrYeJ5as7XM/ZpulX8NEQis44w53h1qAgnC3PRzM7Zc/D6Ibr0rgUathB6zRHP8PYrfgnNOS9PhSBdH' \
89 | 'legK+AGGanfwjXuQ9+0pZcy0w9lQ=='
90 |
91 | crypto = DingTalkCrypto(self.token, self.encoding_aes_key, self.suite_key)
92 | msg = crypto.decrypt_encrypt_str(
93 | signature,
94 | timestamp,
95 | nonce,
96 | encrypt_str
97 | )
98 | msg_dict = json.loads(to_text(msg))
99 | self.assertEqual('check_create_suite_url', msg_dict['EventType'])
100 | self.assertEqual('LPIdSnlF', msg_dict['Random'])
101 | self.assertEqual(self.suite_key, msg_dict['TestSuiteKey'])
102 |
103 | def test_decrypt_encrypt_binary(self):
104 | from dingtalk.core.utils import to_text
105 | signature = '5a65ceeef9aab2d149439f82dc191dd6c5cbe2c0'
106 | timestamp = '1445827045067'
107 | nonce = 'nEXhMP4r'
108 | encrypt_str = b'1a3NBxmCFwkCJvfoQ7WhJHB+iX3qHPsc9JbaDznE1i03peOk1LaOQoRz3+nlyGNhwmwJ3vDMG+OzrHMeiZI7gTRWVdUB' \
109 | b'mfxjZ8Ej23JVYa9VrYeJ5as7XM/ZpulX8NEQis44w53h1qAgnC3PRzM7Zc/D6Ibr0rgUathB6zRHP8PYrfgnNOS9PhSB' \
110 | b'dHlegK+AGGanfwjXuQ9+0pZcy0w9lQ=='
111 |
112 | crypto = DingTalkCrypto(self.token, self.encoding_aes_key, self.suite_key)
113 | msg = crypto.decrypt_encrypt_str(
114 | signature,
115 | timestamp,
116 | nonce,
117 | encrypt_str
118 | )
119 | msg_dict = json.loads(to_text(msg))
120 | self.assertEqual('check_create_suite_url', msg_dict['EventType'])
121 | self.assertEqual('LPIdSnlF', msg_dict['Random'])
122 | self.assertEqual(self.suite_key, msg_dict['TestSuiteKey'])
123 |
124 | def test_decrypt_encrypt_str_should_fail(self):
125 | from dingtalk.core.exceptions import InvalidSignatureException
126 | signature = '5a65ceeef9aab2d149439f82dc191dd6c5cbe2c0'
127 | timestamp = '1445827045067'
128 | nonce = 'xxxxx'
129 | encrypt_str = b'1a3NBxmCFwkCJvfoQ7WhJHB+iX3qHPsc9JbaDznE1i03peOk1LaOQoRz3+nlyGNhwmwJ3vDMG+OzrHMeiZI7gTRWVdUB' \
130 | b'mfxjZ8Ej23JVYa9VrYeJ5as7XM/ZpulX8NEQis44w53h1qAgnC3PRzM7Zc/D6Ibr0rgUathB6zRHP8PYrfgnNOS9PhSB' \
131 | b'dHlegK+AGGanfwjXuQ9+0pZcy0w9lQ=='
132 |
133 | crypto = DingTalkCrypto(self.token, self.encoding_aes_key, self.suite_key)
134 |
135 | self.assertRaises(
136 | InvalidSignatureException,
137 | crypto.decrypt_encrypt_str,
138 | signature, timestamp, nonce, encrypt_str
139 | )
140 |
--------------------------------------------------------------------------------
/tests/test_message.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | import unittest
4 |
5 |
6 | class MessagesTestCase(unittest.TestCase):
7 |
8 | def test_text_message(self):
9 | from dingtalk.model.message import TextBody
10 |
11 | msgbody = TextBody('test')
12 | msg = msgbody.get_dict()
13 |
14 | self.assertEqual('text', msg['msgtype'])
15 | self.assertEqual('test', msg['text']['content'])
16 |
17 | def test_file_message(self):
18 | from dingtalk.model.message import FileBody
19 |
20 | msgbody = FileBody('123456')
21 | msg = msgbody.get_dict()
22 |
23 | self.assertEqual('file', msg['msgtype'])
24 | self.assertEqual('123456', msg['file']['media_id'])
25 |
26 | def test_image_message(self):
27 | from dingtalk.model.message import ImageBody
28 |
29 | msgbody = ImageBody('123456')
30 | msg = msgbody.get_dict()
31 |
32 | self.assertEqual('image', msg['msgtype'])
33 | self.assertEqual('123456', msg['image']['media_id'])
34 |
35 | def test_voice_message(self):
36 | from dingtalk.model.message import VoiceBody
37 |
38 | msgbody = VoiceBody('123456')
39 | msg = msgbody.get_dict()
40 |
41 | self.assertEqual('123456', msgbody.media_id)
42 | self.assertEqual('voice', msg['msgtype'])
43 | self.assertEqual('123456', msg['voice']['media_id'])
44 |
45 | def test_link_message(self):
46 | from dingtalk.model.message import LinkBody
47 |
48 | msgbody = LinkBody('http://dingtalk.com', 'http://dingtalk.com/img.png', 'testtitle', 'testtext')
49 | msg = msgbody.get_dict()
50 |
51 | self.assertEqual('link', msg['msgtype'])
52 | self.assertEqual('http://dingtalk.com', msg['link']['messageUrl'])
53 | self.assertEqual('http://dingtalk.com/img.png', msg['link']['picUrl'])
54 | self.assertEqual('testtitle', msg['link']['title'])
55 | self.assertEqual('testtext', msg['link']['text'])
56 |
57 | def test_markdown_message(self):
58 | from dingtalk.model.message import MarkdownBody
59 |
60 | msgbody = MarkdownBody('title', 'markdowntext')
61 | msg = msgbody.get_dict()
62 |
63 | self.assertEqual('markdown', msg['msgtype'])
64 | self.assertEqual('title', msg['markdown']['title'])
65 | self.assertEqual('markdowntext', msg['markdown']['text'])
66 |
67 | def test_oa_message(self):
68 | from collections import OrderedDict
69 | from dingtalk.model.message import OaBodyContent, OaBody
70 | forms = OrderedDict()
71 | forms['key1'] = 'value1'
72 | forms['key2'] = 'value2'
73 | body_content = OaBodyContent('title', 'content', 'author', '123', '1', forms, '1.12', 'unit')
74 | msgbody = OaBody('http://dingtalk.com', 'ffffff', 'head_text', body_content, 'http://dingtalk.com/index.html')
75 | msg = msgbody.get_dict()
76 |
77 | self.assertEqual('oa', msg['msgtype'])
78 | self.assertEqual('http://dingtalk.com', msg['oa']['message_url'])
79 | self.assertEqual('http://dingtalk.com/index.html', msg['oa']['pc_message_url'])
80 | self.assertEqual('ffffff', msg['oa']['head']['bgcolor'])
81 | self.assertEqual('head_text', msg['oa']['head']['text'])
82 | self.assertEqual('title', msg['oa']['body']['title'])
83 | self.assertEqual('content', msg['oa']['body']['content'])
84 | self.assertEqual('author', msg['oa']['body']['author'])
85 | self.assertEqual('123', msg['oa']['body']['image'])
86 | self.assertEqual('1', msg['oa']['body']['file_count'])
87 | self.assertEqual('1.12', msg['oa']['body']['rich']['num'])
88 | self.assertEqual('unit', msg['oa']['body']['rich']['unit'])
89 | self.assertEqual('key1', msg['oa']['body']['form'][0]['key'])
90 | self.assertEqual('value1', msg['oa']['body']['form'][0]['value'])
91 | self.assertEqual('key2', msg['oa']['body']['form'][1]['key'])
92 | self.assertEqual('value2', msg['oa']['body']['form'][1]['value'])
93 |
94 | def test_single_action_card_message(self):
95 | from dingtalk.model.message import SingleActionCardBody
96 |
97 | msgbody = SingleActionCardBody('title', 'markdown', 'single_title', 'http://dingtalk.com/index.html')
98 | msg = msgbody.get_dict()
99 |
100 | self.assertEqual('action_card', msg['msgtype'])
101 | self.assertEqual('title', msg['action_card']['title'])
102 | self.assertEqual('markdown', msg['action_card']['markdown'])
103 | self.assertEqual('single_title', msg['action_card']['single_title'])
104 | self.assertEqual('title', msg['action_card']['title'])
105 | self.assertEqual('http://dingtalk.com/index.html', msg['action_card']['single_url'])
106 |
107 | def test_button_action_card_message(self):
108 | from dingtalk.model.message import BtnActionCardBody
109 |
110 | msgbody = BtnActionCardBody('title', 'markdown', '0', [{'title': 'title1', 'action_url': 'action_url1'},
111 | {'title': 'title2', 'action_url': 'action_url2'}])
112 | msgbody.add_btn('title3', 'action_url3')
113 | msg = msgbody.get_dict()
114 |
115 | self.assertEqual('action_card', msg['msgtype'])
116 | self.assertEqual('title', msg['action_card']['title'])
117 | self.assertEqual('markdown', msg['action_card']['markdown'])
118 | self.assertEqual('0', msg['action_card']['btn_orientation'])
119 | self.assertEqual('title1', msg['action_card']['btn_json_list'][0]['title'])
120 | self.assertEqual('action_url1', msg['action_card']['btn_json_list'][0]['action_url'])
121 | self.assertEqual('title2', msg['action_card']['btn_json_list'][1]['title'])
122 | self.assertEqual('action_url2', msg['action_card']['btn_json_list'][1]['action_url'])
123 | self.assertEqual('title3', msg['action_card']['btn_json_list'][2]['title'])
124 | self.assertEqual('action_url3', msg['action_card']['btn_json_list'][2]['action_url'])
125 |
--------------------------------------------------------------------------------
/tests/test_storage.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | import unittest
4 |
5 |
6 | class WeChatSessionTestCase(unittest.TestCase):
7 |
8 | def test_dingtalk_cache(self, storage=None):
9 | from dingtalk.storage.cache import DingTalkCache
10 | if storage is None:
11 | return
12 | cache = DingTalkCache(storage)
13 | cache.access_token.set('crop', 'test1', 7200)
14 | cache.jsapi_ticket.set('crop', 'test2', 7200)
15 |
16 | self.assertEqual('test1', cache.access_token.get('crop'))
17 | self.assertEqual('test2', cache.jsapi_ticket.get('crop'))
18 |
19 | def test_channel_cache(self, storage=None):
20 | from dingtalk.storage.cache import ChannelCache
21 | if storage is None:
22 | return
23 | cache = ChannelCache(storage)
24 | cache.channel_token.set('crop', 'test3', 7200)
25 | cache.jsapi_ticket.set('crop', 'test4', 7200)
26 |
27 | self.assertEqual('test3', cache.channel_token.get('crop'))
28 | self.assertEqual('test4', cache.jsapi_ticket.get('crop'))
29 |
30 | def test_isv_cache(self, storage=None):
31 | from dingtalk.storage.cache import ISVCache
32 | if storage is None:
33 | return
34 | cache = ISVCache(storage)
35 | cache.suite_ticket.set('crop', 'test5', 7200)
36 | cache.suite_access_token.set('crop', 'test6', 7200)
37 | cache.permanent_code.set('crop', 'test7', 7200)
38 | cache.ch_permanent_code.set('crop', 'test8', 7200)
39 |
40 | self.assertEqual('test5', cache.suite_ticket.get('crop'))
41 | self.assertEqual('test6', cache.suite_access_token.get('crop'))
42 | self.assertEqual('test7', cache.permanent_code.get('crop'))
43 | self.assertEqual('test8', cache.ch_permanent_code.get('crop'))
44 |
45 | def test_caches(self, storage=None):
46 | if storage is None:
47 | return
48 | self.test_dingtalk_cache(storage)
49 | self.test_channel_cache(storage)
50 | self.test_isv_cache(storage)
51 |
52 | def test_memory_storage(self):
53 | from dingtalk.storage.memorystorage import MemoryStorage
54 |
55 | storage = MemoryStorage()
56 | self.test_caches(storage)
57 |
58 | def test_redis_storage(self):
59 | from redis import Redis
60 | from dingtalk.storage.kvstorage import KvStorage
61 | redis = Redis()
62 | storage = KvStorage(redis)
63 | self.test_caches(storage)
64 |
65 | def test_memcache_storage(self):
66 | from pymemcache.client import Client
67 | from dingtalk.storage.kvstorage import KvStorage
68 | servers = ("127.0.0.1", 11211)
69 | memcached = Client(servers)
70 | storage = KvStorage(memcached)
71 | self.test_caches(storage)
72 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 | import unittest
4 |
5 | from dingtalk.core.utils import ObjectDict, DingTalkSigner
6 |
7 |
8 | class UtilityTestCase(unittest.TestCase):
9 |
10 | def test_object_dict(self):
11 | obj = ObjectDict()
12 | self.assertTrue(obj.xxx is None)
13 | obj.xxx = 1
14 | self.assertEqual(1, obj.xxx)
15 |
16 | def test_wechat_card_signer(self):
17 |
18 | signer = DingTalkSigner()
19 | signer.add_data('789')
20 | signer.add_data('456')
21 | signer.add_data('123')
22 | signature = signer.signature
23 |
24 | self.assertEqual('f7c3bc1d808e04732adf679965ccc34ca7ae3441', signature)
25 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = {py27,py34,py35,py36,pypy,pypy3}-{cryptography,pycrypto}
3 |
4 | [testenv]
5 | usedevelop = True
6 | basepython =
7 | py27: python2.7
8 | py34: python3.4
9 | py35: python3.5
10 | py36: python3.6
11 | pypy: pypy
12 | pypy3: pypy3
13 | deps =
14 | -rdev-requirements.txt
15 | commands =
16 | pytest -v
17 |
18 | [testenv:py27-cryptography]
19 | extras=cryptography
20 |
21 | [testenv:py27-pycrypto]
22 | extras=pycrypto
23 |
24 | [testenv:py34-cryptography]
25 | extras=cryptography
26 |
27 | [testenv:py34-pycrypto]
28 | extras=pycrypto
29 |
30 | [testenv:py35-cryptography]
31 | extras=cryptography
32 |
33 | [testenv:py35-pycrypto]
34 | extras=pycrypto
35 |
36 | [testenv:py36-cryptography]
37 | extras=cryptography
38 |
39 | [testenv:py36-pycrypto]
40 | extras=pycrypto
41 |
42 | [testenv:pypy-cryptography]
43 | extras=cryptography
44 |
45 | [testenv:pypy-pycrypto]
46 | extras=pycrypto
47 |
48 | [testenv:pypy3-cryptography]
49 | extras=cryptography
50 |
51 | [testenv:pypy3-pycrypto]
52 | extras=pycrypto
53 |
--------------------------------------------------------------------------------