codosdk 1.0.56__tar.gz → 1.0.57__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {codosdk-1.0.56 → codosdk-1.0.57}/PKG-INFO +1 -1
  2. {codosdk-1.0.56 → codosdk-1.0.57}/README.md +16 -0
  3. {codosdk-1.0.56 → codosdk-1.0.57}/codosdk.egg-info/PKG-INFO +1 -1
  4. {codosdk-1.0.56 → codosdk-1.0.57}/codosdk.egg-info/SOURCES.txt +4 -0
  5. {codosdk-1.0.56 → codosdk-1.0.57}/codosdk.egg-info/top_level.txt +1 -0
  6. {codosdk-1.0.56 → codosdk-1.0.57}/setup.py +2 -2
  7. codosdk-1.0.57/tests/test_openapi_sign.py +41 -0
  8. codosdk-1.0.57/websdk2/cloud/__init__.py +0 -0
  9. codosdk-1.0.57/websdk2/openapi_client.py +127 -0
  10. codosdk-1.0.57/websdk2/openapi_sign.py +120 -0
  11. {codosdk-1.0.56 → codosdk-1.0.57}/LICENSE +0 -0
  12. {codosdk-1.0.56 → codosdk-1.0.57}/codosdk.egg-info/dependency_links.txt +0 -0
  13. {codosdk-1.0.56 → codosdk-1.0.57}/codosdk.egg-info/requires.txt +0 -0
  14. {codosdk-1.0.56 → codosdk-1.0.57}/opssdk/__init__.py +0 -0
  15. {codosdk-1.0.56 → codosdk-1.0.57}/opssdk/utils/__init__.py +0 -0
  16. {codosdk-1.0.56 → codosdk-1.0.57}/pyproject.toml +0 -0
  17. {codosdk-1.0.56 → codosdk-1.0.57}/setup.cfg +0 -0
  18. {codosdk-1.0.56/websdk2 → codosdk-1.0.57/tests}/__init__.py +0 -0
  19. {codosdk-1.0.56/websdk2/cloud → codosdk-1.0.57/websdk2}/__init__.py +0 -0
  20. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/api_set.py +0 -0
  21. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/__init__.py +0 -0
  22. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/admin_apis.py +0 -0
  23. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/agent_apis.py +0 -0
  24. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/cmdb_apis.py +0 -0
  25. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/kerrigan_apis.py +0 -0
  26. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/mgv4_apis.py +0 -0
  27. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/notice_apis.py +0 -0
  28. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/apis/task_apis.py +0 -0
  29. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/application.py +0 -0
  30. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/base_handler.py +0 -0
  31. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/cache.py +0 -0
  32. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/cache_context.py +0 -0
  33. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/client.py +0 -0
  34. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/cloud/qcloud_api.py +0 -0
  35. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/cloud/ucloud_api.py +0 -0
  36. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/cloud_utils.py +0 -0
  37. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/configs.py +0 -0
  38. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/consts.py +0 -0
  39. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/crud_utils.py +0 -0
  40. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/db_context.py +0 -0
  41. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/error.py +0 -0
  42. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/fetch_coroutine.py +0 -0
  43. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/jwt_token.py +0 -0
  44. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/ldap.py +0 -0
  45. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/logger.py +0 -0
  46. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/model_utils.py +0 -0
  47. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/mqhelper.py +0 -0
  48. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/program.py +0 -0
  49. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/salt_api.py +0 -0
  50. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/sqlalchemy_pagination.py +0 -0
  51. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/tools.py +0 -0
  52. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/utils/__init__.py +0 -0
  53. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/utils/cc_crypto.py +0 -0
  54. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/utils/date_format.py +0 -0
  55. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/utils/pydantic_utils.py +0 -0
  56. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/utils/pydantic_utils_v2.py +0 -0
  57. {codosdk-1.0.56 → codosdk-1.0.57}/websdk2/web_logs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codosdk
3
- Version: 1.0.56
3
+ Version: 1.0.57
4
4
  Summary: CODO项目的Python SDK
5
5
  Home-page: https://github.com/ss1917/codo_sdk/
6
6
  Author: shenshuo
@@ -99,6 +99,22 @@ token = auth.encode_auth_token(
99
99
  user_info = auth.decode_auth_token(token)
100
100
  ```
101
101
 
102
+ ### 开放 API(AK/SK)
103
+
104
+ 适用于 CI、集成方等编程访问。网关校验 `CODO1-HMAC-SHA256` 签名后签发短时内部 JWT。
105
+
106
+ ```python
107
+ from websdk2.openapi_client import OpenAPIClient
108
+
109
+ client = OpenAPIClient(
110
+ endpoint="https://gw.example.com",
111
+ access_key="your-ak",
112
+ secret_key="your-sk",
113
+ )
114
+ resp = client.request("GET", "/api/p/v4/biz/list/")
115
+ print(resp.status_code, resp.text)
116
+ ```
117
+
102
118
  ### 消息队列
103
119
 
104
120
  ```python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codosdk
3
- Version: 1.0.56
3
+ Version: 1.0.57
4
4
  Summary: CODO项目的Python SDK
5
5
  Home-page: https://github.com/ss1917/codo_sdk/
6
6
  Author: shenshuo
@@ -10,6 +10,8 @@ codosdk.egg-info/requires.txt
10
10
  codosdk.egg-info/top_level.txt
11
11
  opssdk/__init__.py
12
12
  opssdk/utils/__init__.py
13
+ tests/__init__.py
14
+ tests/test_openapi_sign.py
13
15
  websdk2/__init__.py
14
16
  websdk2/api_set.py
15
17
  websdk2/application.py
@@ -29,6 +31,8 @@ websdk2/ldap.py
29
31
  websdk2/logger.py
30
32
  websdk2/model_utils.py
31
33
  websdk2/mqhelper.py
34
+ websdk2/openapi_client.py
35
+ websdk2/openapi_sign.py
32
36
  websdk2/program.py
33
37
  websdk2/salt_api.py
34
38
  websdk2/sqlalchemy_pagination.py
@@ -1,2 +1,3 @@
1
1
  opssdk
2
+ tests
2
3
  websdk2
@@ -2,14 +2,14 @@
2
2
  # -*-coding:utf-8-*-
3
3
  """"
4
4
  author : shenshuo
5
- date : 20241212
5
+ date : 20260729
6
6
  desc : CODO SDK
7
7
  """
8
8
 
9
9
  import sys
10
10
  from setuptools import setup, find_packages
11
11
 
12
- VERSION = '1.0.56'
12
+ VERSION = '1.0.57'
13
13
 
14
14
  if sys.version_info < (2, 7) or (3, 0) <= sys.version_info < (3, 6):
15
15
  print('This program requires at least Python 2.7 or 3.6 to run.')
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ from websdk2.openapi_sign import build_auth_headers, sign
4
+
5
+
6
+ def test_get_vector():
7
+ ak = "codoAKTEST0001"
8
+ sk = "testSecretKeyForOpenAPISign0001"
9
+ ts = "1700000000"
10
+ nonce = "nonce-fixed-001"
11
+ headers = {
12
+ "x-codo-access-key": ak,
13
+ "x-codo-timestamp": ts,
14
+ "x-codo-nonce": nonce,
15
+ }
16
+ sig = sign(sk, "GET", "/api/p/v4/biz/list/", {}, headers, b"", ts)
17
+ assert sig == "ce94e413a8f752ed75a7c129c184f14892d8b24f30222607994276174339e07f"
18
+
19
+
20
+ def test_post_vector():
21
+ ak = "codoAKTEST0001"
22
+ sk = "testSecretKeyForOpenAPISign0001"
23
+ ts = "1700000000"
24
+ nonce = "nonce-fixed-001"
25
+ headers = {
26
+ "content-type": "application/json",
27
+ "x-codo-access-key": ak,
28
+ "x-codo-timestamp": ts,
29
+ "x-codo-nonce": nonce,
30
+ }
31
+ body = b'{"page":1}'
32
+ sig = sign(sk, "POST", "/api/p/v4/user/", {}, headers, body, ts)
33
+ assert sig == "4a2adef69ff58b92bc99c04474811e78082a5e3f5675a3c1d26e19ed3e7dc163"
34
+
35
+
36
+ def test_build_auth_headers_keys():
37
+ h = build_auth_headers(
38
+ "ak", "sk", "GET", "/api/x/", query=None, body=None, timestamp=1700000000, nonce="n1"
39
+ )
40
+ assert "X-Codo-Access-Key" in h
41
+ assert "X-Codo-Signature" in h
File without changes
@@ -0,0 +1,127 @@
1
+ # -*- coding: utf-8 -*-
2
+ """开放 API 客户端:AK/SK 签名请求 CODO 网关。"""
3
+
4
+ import json
5
+ import logging
6
+ from typing import Any, Dict, Optional
7
+ from urllib.parse import urlencode, urlparse
8
+ import requests
9
+ from .configs import configs
10
+ from .consts import const
11
+ from .openapi_sign import build_auth_headers
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OpenAPIClient:
17
+ def __init__(
18
+ self,
19
+ access_key: str,
20
+ secret_key: str,
21
+ endpoint: str = None,
22
+ request_timeout: int = 10,
23
+ ):
24
+ self.access_key = access_key
25
+ self.secret_key = secret_key
26
+ self.request_timeout = request_timeout
27
+ if endpoint:
28
+ self.endpoint = endpoint.rstrip("/")
29
+ else:
30
+ self.endpoint = (configs.get(const.WEBSITE_API_GW_URL) or "http://gw.opendevops.cn").rstrip(
31
+ "/"
32
+ )
33
+
34
+ def _path_and_query(self, url: str, params: Optional[dict]):
35
+ if url.startswith("http://") or url.startswith("https://"):
36
+ parsed = urlparse(url)
37
+ path = parsed.path or "/"
38
+ # merge query from url
39
+ q = {}
40
+ if parsed.query:
41
+ from urllib.parse import parse_qs
42
+
43
+ for k, vs in parse_qs(parsed.query, keep_blank_values=True).items():
44
+ q[k] = vs[0] if len(vs) == 1 else vs
45
+ if params:
46
+ q.update(params)
47
+ return path, q, f"{parsed.scheme}://{parsed.netloc}"
48
+ path = url if url.startswith("/") else "/" + url
49
+ return path, params or {}, self.endpoint
50
+
51
+ def request(
52
+ self,
53
+ method: str,
54
+ url: str,
55
+ params: dict = None,
56
+ data: Any = None,
57
+ json_body: Any = None,
58
+ headers: dict = None,
59
+ ) -> requests.Response:
60
+ path, query, base = self._path_and_query(url, params)
61
+ method_u = method.upper()
62
+ body_bytes = None
63
+ content_type = None
64
+ if json_body is not None:
65
+ body_bytes = json.dumps(json_body, ensure_ascii=False, separators=(",", ":")).encode(
66
+ "utf-8"
67
+ )
68
+ content_type = "application/json"
69
+ elif data is not None:
70
+ if isinstance(data, (dict, list)):
71
+ body_bytes = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode(
72
+ "utf-8"
73
+ )
74
+ content_type = "application/json"
75
+ elif isinstance(data, str):
76
+ body_bytes = data.encode("utf-8")
77
+ else:
78
+ body_bytes = data
79
+
80
+ auth_headers = build_auth_headers(
81
+ self.access_key,
82
+ self.secret_key,
83
+ method_u,
84
+ path,
85
+ query=query,
86
+ body=body_bytes,
87
+ content_type=content_type,
88
+ )
89
+ final_headers = dict(headers or {})
90
+ final_headers.update(auth_headers)
91
+
92
+ full_url = f"{base}{path}"
93
+ if query:
94
+ full_url = f"{full_url}?{urlencode(query, doseq=True)}"
95
+
96
+ return requests.request(
97
+ method_u,
98
+ full_url,
99
+ headers=final_headers,
100
+ data=body_bytes,
101
+ timeout=self.request_timeout,
102
+ )
103
+
104
+ def with_params_data_url(self, **kwargs):
105
+ url = kwargs.get("url")
106
+ method = kwargs.get("method") or "GET"
107
+ params = kwargs.get("params")
108
+ body = kwargs.get("body")
109
+ req_json = kwargs.get("json")
110
+ if method.upper() in ("GET", "DELETE", "HEAD"):
111
+ return dict(method=method, url=url, params=params)
112
+ return dict(method=method, url=url, params=params, body=body, json=req_json)
113
+
114
+ def do_action(self, **kwargs) -> str:
115
+ return self.do_action_v2(**kwargs).text
116
+
117
+ def do_action_v2(self, **kwargs) -> requests.Response:
118
+ return self.do_action_v3(**kwargs)
119
+
120
+ def do_action_v3(self, **kwargs) -> requests.Response:
121
+ method = kwargs.get("method") or "GET"
122
+ url = kwargs.get("url")
123
+ params = kwargs.get("params")
124
+ if kwargs.get("json") is not None:
125
+ return self.request(method, url, params=params, json_body=kwargs.get("json"))
126
+ body = kwargs.get("body")
127
+ return self.request(method, url, params=params, data=body)
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """CODO1-HMAC-SHA256 签名(与网关 / Go SDK 对齐)。"""
4
+
5
+ import hashlib
6
+ import hmac
7
+ import secrets
8
+ import time
9
+ from typing import Dict, Mapping, Optional, Union
10
+ from urllib.parse import quote
11
+
12
+
13
+ def sha256_hex(data: Union[bytes, str, None]) -> str:
14
+ if data is None:
15
+ data = b""
16
+ if isinstance(data, str):
17
+ data = data.encode("utf-8")
18
+ return hashlib.sha256(data).hexdigest()
19
+
20
+
21
+ def hmac_sha256_hex(key: str, msg: str) -> str:
22
+ return hmac.new(key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest()
23
+
24
+
25
+ def canonical_query(params: Optional[Mapping] = None) -> str:
26
+ if not params:
27
+ return ""
28
+ items = []
29
+ for k in sorted(params.keys()):
30
+ v = params[k]
31
+ if isinstance(v, (list, tuple)):
32
+ for vv in sorted(str(x) for x in v):
33
+ items.append(f"{quote(str(k), safe='-_.~')}={quote(str(vv), safe='-_.~')}")
34
+ elif v is None:
35
+ continue
36
+ else:
37
+ items.append(f"{quote(str(k), safe='-_.~')}={quote(str(v), safe='-_.~')}")
38
+ return "&".join(items)
39
+
40
+
41
+ def build_canonical_request(
42
+ method: str,
43
+ uri: str,
44
+ query: Optional[Mapping],
45
+ headers: Mapping[str, str],
46
+ body: Union[bytes, str, None],
47
+ ) -> str:
48
+ if body is None:
49
+ body_b = b""
50
+ elif isinstance(body, str):
51
+ body_b = body.encode("utf-8")
52
+ else:
53
+ body_b = body
54
+ normalized = {str(k).lower(): str(v).strip() for k, v in headers.items()}
55
+ names = sorted(normalized.keys())
56
+ canonical_headers = "".join(f"{n}:{normalized[n]}\n" for n in names)
57
+ signed_headers = ";".join(names)
58
+ return "\n".join(
59
+ [
60
+ method.upper(),
61
+ uri,
62
+ canonical_query(query),
63
+ canonical_headers,
64
+ signed_headers,
65
+ sha256_hex(body_b),
66
+ ]
67
+ )
68
+
69
+
70
+ def build_string_to_sign(timestamp: Union[str, int], canonical_request: str) -> str:
71
+ return "\n".join(
72
+ ["CODO1-HMAC-SHA256", str(timestamp), sha256_hex(canonical_request.encode("utf-8"))]
73
+ )
74
+
75
+
76
+ def sign(
77
+ secret_key: str,
78
+ method: str,
79
+ uri: str,
80
+ query: Optional[Mapping],
81
+ headers: Mapping[str, str],
82
+ body: Union[bytes, str, None],
83
+ timestamp: Union[str, int],
84
+ ) -> str:
85
+ cr = build_canonical_request(method, uri, query, headers, body)
86
+ sts = build_string_to_sign(timestamp, cr)
87
+ return hmac_sha256_hex(secret_key, sts)
88
+
89
+
90
+ def build_auth_headers(
91
+ access_key: str,
92
+ secret_key: str,
93
+ method: str,
94
+ uri: str,
95
+ query: Optional[Mapping] = None,
96
+ body: Union[bytes, str, None] = None,
97
+ content_type: Optional[str] = None,
98
+ timestamp: Optional[Union[str, int]] = None,
99
+ nonce: Optional[str] = None,
100
+ ) -> Dict[str, str]:
101
+ ts = str(timestamp if timestamp is not None else int(time.time()))
102
+ n = nonce or secrets.token_hex(8)
103
+ sign_headers = {
104
+ "x-codo-access-key": access_key,
105
+ "x-codo-timestamp": ts,
106
+ "x-codo-nonce": n,
107
+ }
108
+ body_b = b"" if body is None else (body.encode("utf-8") if isinstance(body, str) else body)
109
+ if content_type and body_b:
110
+ sign_headers["content-type"] = content_type
111
+ signature = sign(secret_key, method, uri, query, sign_headers, body_b, ts)
112
+ out = {
113
+ "X-Codo-Access-Key": access_key,
114
+ "X-Codo-Timestamp": ts,
115
+ "X-Codo-Nonce": n,
116
+ "X-Codo-Signature": signature,
117
+ }
118
+ if content_type:
119
+ out["Content-Type"] = content_type
120
+ return out
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes