ul-api-utils 9.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. example/__init__.py +0 -0
  2. example/conf.py +35 -0
  3. example/main.py +24 -0
  4. example/models/__init__.py +0 -0
  5. example/permissions.py +6 -0
  6. example/pure_flask_example.py +65 -0
  7. example/rate_limit_load.py +10 -0
  8. example/redis_repository.py +22 -0
  9. example/routes/__init__.py +0 -0
  10. example/routes/api_some.py +335 -0
  11. example/sockets/__init__.py +0 -0
  12. example/sockets/on_connect.py +16 -0
  13. example/sockets/on_disconnect.py +14 -0
  14. example/sockets/on_json.py +10 -0
  15. example/sockets/on_message.py +13 -0
  16. example/sockets/on_open.py +16 -0
  17. example/workers/__init__.py +0 -0
  18. example/workers/worker.py +28 -0
  19. ul_api_utils/__init__.py +0 -0
  20. ul_api_utils/access/__init__.py +122 -0
  21. ul_api_utils/api_resource/__init__.py +0 -0
  22. ul_api_utils/api_resource/api_request.py +105 -0
  23. ul_api_utils/api_resource/api_resource.py +414 -0
  24. ul_api_utils/api_resource/api_resource_config.py +20 -0
  25. ul_api_utils/api_resource/api_resource_error_handling.py +21 -0
  26. ul_api_utils/api_resource/api_resource_fn_typing.py +356 -0
  27. ul_api_utils/api_resource/api_resource_type.py +16 -0
  28. ul_api_utils/api_resource/api_response.py +300 -0
  29. ul_api_utils/api_resource/api_response_db.py +26 -0
  30. ul_api_utils/api_resource/api_response_payload_alias.py +25 -0
  31. ul_api_utils/api_resource/db_types.py +9 -0
  32. ul_api_utils/api_resource/signature_check.py +41 -0
  33. ul_api_utils/commands/__init__.py +0 -0
  34. ul_api_utils/commands/cmd_enc_keys.py +172 -0
  35. ul_api_utils/commands/cmd_gen_api_user_token.py +77 -0
  36. ul_api_utils/commands/cmd_gen_new_api_user.py +106 -0
  37. ul_api_utils/commands/cmd_generate_api_docs.py +181 -0
  38. ul_api_utils/commands/cmd_start.py +110 -0
  39. ul_api_utils/commands/cmd_worker_start.py +76 -0
  40. ul_api_utils/commands/start/__init__.py +0 -0
  41. ul_api_utils/commands/start/gunicorn.conf.local.py +0 -0
  42. ul_api_utils/commands/start/gunicorn.conf.py +26 -0
  43. ul_api_utils/commands/start/wsgi.py +22 -0
  44. ul_api_utils/conf/ul-debugger-main.js +1 -0
  45. ul_api_utils/conf/ul-debugger-ui.js +1 -0
  46. ul_api_utils/conf.py +70 -0
  47. ul_api_utils/const.py +78 -0
  48. ul_api_utils/debug/__init__.py +0 -0
  49. ul_api_utils/debug/debugger.py +119 -0
  50. ul_api_utils/debug/malloc.py +93 -0
  51. ul_api_utils/debug/stat.py +444 -0
  52. ul_api_utils/encrypt/__init__.py +0 -0
  53. ul_api_utils/encrypt/encrypt_decrypt_abstract.py +15 -0
  54. ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py +59 -0
  55. ul_api_utils/errors.py +200 -0
  56. ul_api_utils/internal_api/__init__.py +0 -0
  57. ul_api_utils/internal_api/__tests__/__init__.py +0 -0
  58. ul_api_utils/internal_api/__tests__/internal_api.py +29 -0
  59. ul_api_utils/internal_api/__tests__/internal_api_content_type.py +22 -0
  60. ul_api_utils/internal_api/internal_api.py +369 -0
  61. ul_api_utils/internal_api/internal_api_check_context.py +42 -0
  62. ul_api_utils/internal_api/internal_api_error.py +17 -0
  63. ul_api_utils/internal_api/internal_api_response.py +296 -0
  64. ul_api_utils/main.py +29 -0
  65. ul_api_utils/modules/__init__.py +0 -0
  66. ul_api_utils/modules/__tests__/__init__.py +0 -0
  67. ul_api_utils/modules/__tests__/test_api_sdk_jwt.py +195 -0
  68. ul_api_utils/modules/api_sdk.py +555 -0
  69. ul_api_utils/modules/api_sdk_config.py +63 -0
  70. ul_api_utils/modules/api_sdk_jwt.py +377 -0
  71. ul_api_utils/modules/intermediate_state.py +34 -0
  72. ul_api_utils/modules/worker_context.py +35 -0
  73. ul_api_utils/modules/worker_sdk.py +109 -0
  74. ul_api_utils/modules/worker_sdk_config.py +13 -0
  75. ul_api_utils/py.typed +0 -0
  76. ul_api_utils/resources/__init__.py +0 -0
  77. ul_api_utils/resources/caching.py +196 -0
  78. ul_api_utils/resources/debugger_scripts.py +97 -0
  79. ul_api_utils/resources/health_check/__init__.py +0 -0
  80. ul_api_utils/resources/health_check/const.py +2 -0
  81. ul_api_utils/resources/health_check/health_check.py +439 -0
  82. ul_api_utils/resources/health_check/health_check_template.py +64 -0
  83. ul_api_utils/resources/health_check/resource.py +97 -0
  84. ul_api_utils/resources/not_implemented.py +25 -0
  85. ul_api_utils/resources/permissions.py +29 -0
  86. ul_api_utils/resources/rate_limitter.py +84 -0
  87. ul_api_utils/resources/socketio.py +55 -0
  88. ul_api_utils/resources/swagger.py +119 -0
  89. ul_api_utils/resources/web_forms/__init__.py +0 -0
  90. ul_api_utils/resources/web_forms/custom_fields/__init__.py +0 -0
  91. ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py +5 -0
  92. ul_api_utils/resources/web_forms/custom_widgets/__init__.py +0 -0
  93. ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py +86 -0
  94. ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py +42 -0
  95. ul_api_utils/resources/web_forms/uni_form.py +75 -0
  96. ul_api_utils/sentry.py +52 -0
  97. ul_api_utils/utils/__init__.py +0 -0
  98. ul_api_utils/utils/__tests__/__init__.py +0 -0
  99. ul_api_utils/utils/__tests__/api_path_version.py +16 -0
  100. ul_api_utils/utils/__tests__/unwrap_typing.py +67 -0
  101. ul_api_utils/utils/api_encoding.py +51 -0
  102. ul_api_utils/utils/api_format.py +61 -0
  103. ul_api_utils/utils/api_method.py +55 -0
  104. ul_api_utils/utils/api_pagination.py +58 -0
  105. ul_api_utils/utils/api_path_version.py +60 -0
  106. ul_api_utils/utils/api_request_info.py +6 -0
  107. ul_api_utils/utils/avro.py +131 -0
  108. ul_api_utils/utils/broker_topics_message_count.py +47 -0
  109. ul_api_utils/utils/cached_per_request.py +23 -0
  110. ul_api_utils/utils/colors.py +31 -0
  111. ul_api_utils/utils/constants.py +7 -0
  112. ul_api_utils/utils/decode_base64.py +9 -0
  113. ul_api_utils/utils/deprecated.py +19 -0
  114. ul_api_utils/utils/flags.py +29 -0
  115. ul_api_utils/utils/flask_swagger_generator/__init__.py +0 -0
  116. ul_api_utils/utils/flask_swagger_generator/conf.py +4 -0
  117. ul_api_utils/utils/flask_swagger_generator/exceptions.py +7 -0
  118. ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py +0 -0
  119. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py +57 -0
  120. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py +48 -0
  121. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py +777 -0
  122. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py +40 -0
  123. ul_api_utils/utils/flask_swagger_generator/utils/__init__.py +0 -0
  124. ul_api_utils/utils/flask_swagger_generator/utils/input_type.py +77 -0
  125. ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py +51 -0
  126. ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py +18 -0
  127. ul_api_utils/utils/flask_swagger_generator/utils/request_type.py +52 -0
  128. ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py +15 -0
  129. ul_api_utils/utils/flask_swagger_generator/utils/security_type.py +39 -0
  130. ul_api_utils/utils/imports.py +16 -0
  131. ul_api_utils/utils/instance_checks.py +16 -0
  132. ul_api_utils/utils/jinja/__init__.py +0 -0
  133. ul_api_utils/utils/jinja/t_url_for.py +19 -0
  134. ul_api_utils/utils/jinja/to_pretty_json.py +11 -0
  135. ul_api_utils/utils/json_encoder.py +126 -0
  136. ul_api_utils/utils/load_modules.py +15 -0
  137. ul_api_utils/utils/memory_db/__init__.py +0 -0
  138. ul_api_utils/utils/memory_db/__tests__/__init__.py +0 -0
  139. ul_api_utils/utils/memory_db/errors.py +8 -0
  140. ul_api_utils/utils/memory_db/repository.py +102 -0
  141. ul_api_utils/utils/token_check.py +14 -0
  142. ul_api_utils/utils/token_check_through_request.py +16 -0
  143. ul_api_utils/utils/unwrap_typing.py +117 -0
  144. ul_api_utils/utils/uuid_converter.py +22 -0
  145. ul_api_utils/validators/__init__.py +0 -0
  146. ul_api_utils/validators/__tests__/__init__.py +0 -0
  147. ul_api_utils/validators/__tests__/test_custom_fields.py +32 -0
  148. ul_api_utils/validators/custom_fields.py +66 -0
  149. ul_api_utils/validators/validate_empty_object.py +10 -0
  150. ul_api_utils/validators/validate_uuid.py +11 -0
  151. ul_api_utils-9.3.0.dist-info/LICENSE +21 -0
  152. ul_api_utils-9.3.0.dist-info/METADATA +279 -0
  153. ul_api_utils-9.3.0.dist-info/RECORD +156 -0
  154. ul_api_utils-9.3.0.dist-info/WHEEL +5 -0
  155. ul_api_utils-9.3.0.dist-info/entry_points.txt +2 -0
  156. ul_api_utils-9.3.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,26 @@
1
+ from typing import Dict, Union, Optional, Any, Iterable, Generic, TypeVar, Type, List
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from ul_api_utils.api_resource.api_response import AnyJsonApiResponse
6
+ from ul_api_utils.api_resource.db_types import TDictable
7
+
8
+
9
+ class AnyJsonDbApiResponse(AnyJsonApiResponse):
10
+ payload: Union[Optional[TDictable], Iterable[TDictable]]
11
+
12
+
13
+ TJsonObjApiResponsePayload = TypeVar('TJsonObjApiResponsePayload')
14
+
15
+
16
+ class JsonDbApiResponse(Generic[TJsonObjApiResponsePayload], AnyJsonApiResponse):
17
+
18
+ @classmethod
19
+ def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
20
+ class _ResponseStd(BaseModel):
21
+ ok: bool
22
+ payload: inner_type # type: ignore
23
+ errors: List[Dict[str, Any]]
24
+ total_count: Optional[int] = None
25
+ count: Optional[int] = None
26
+ return _ResponseStd
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import UUID4
4
+
5
+ from ul_api_utils.api_resource.api_response import JsonApiResponsePayload, RootJsonApiResponsePayload
6
+
7
+
8
+ class ApiBaseModelPayloadResponse(JsonApiResponsePayload):
9
+ id: UUID4
10
+ date_created: datetime
11
+ date_modified: datetime
12
+ is_alive: bool
13
+
14
+
15
+ class ApiBaseUserModelPayloadResponse(JsonApiResponsePayload):
16
+ id: UUID4
17
+ date_created: datetime
18
+ date_modified: datetime
19
+ user_created_id: UUID4
20
+ user_modified_id: UUID4
21
+ is_alive: bool
22
+
23
+
24
+ class ApiEmptyResponse(RootJsonApiResponsePayload[None]):
25
+ pass
@@ -0,0 +1,9 @@
1
+ from typing import Union, Dict, Any, Tuple, Optional, Iterable
2
+
3
+ from pydantic import BaseModel
4
+ from ul_db_utils.model.base_model import BaseModel as DbBaseModel
5
+ from flask_sqlalchemy.model import Model
6
+
7
+ # TODO: remove DbBaseModel/Model from it BECAUSE IT loads sqlalchemy (>20mb of code)
8
+ TDictable = Union[Dict[str, Any], BaseModel, Tuple[Any, ...], DbBaseModel, Model]
9
+ TPayloadInputUnion = Union[Optional[TDictable], Iterable[TDictable]]
@@ -0,0 +1,41 @@
1
+ from typing import Any, Dict, Optional, Union, Type, TypeVar, Tuple, TYPE_CHECKING, get_origin, get_args
2
+ from typing import _GenericAlias # type: ignore
3
+
4
+ from pydantic import BaseModel, RootModel
5
+
6
+ from ul_api_utils.utils.json_encoder import to_dict
7
+
8
+ if TYPE_CHECKING:
9
+ from ul_api_utils.api_resource.db_types import TDictable
10
+
11
+ TPydanticModel = TypeVar('TPydanticModel', bound=BaseModel)
12
+
13
+
14
+ def set_model(model: Type[TPydanticModel], data: Union[Dict[str, Any], TPydanticModel]) -> TPydanticModel:
15
+ if isinstance(data, model):
16
+ return data
17
+ if issubclass(model, RootModel):
18
+ return model(data).root
19
+ assert isinstance(data, dict), f'data must be dict. "{type(data).__name__}" was given'
20
+ return model(**data)
21
+
22
+
23
+ def set_model_dictable(model: Type[TPydanticModel], data: 'TDictable') -> Optional[TPydanticModel]:
24
+ if isinstance(data, model):
25
+ return data
26
+ res: Optional[Dict[str, Any]] = to_dict(data)
27
+ if res is None:
28
+ return None
29
+ if issubclass(model, RootModel):
30
+ return model(data).root
31
+ return model(**res)
32
+
33
+
34
+ def get_typing(t: Type[Any]) -> Tuple[Type[Any], ...]:
35
+ if type(t) == _GenericAlias: # noqa: E721
36
+ return get_origin(t), *(it for it in get_args(t))
37
+ if t.__class__.__name__ == 'ModelMetaclass':
38
+ if hasattr(t, '_generic_params') and t._generic_params is not None:
39
+ unspecialized_class = t.__bases__[0]
40
+ return unspecialized_class, t._generic_params[0]
41
+ return t, # noqa: C818
File without changes
@@ -0,0 +1,172 @@
1
+ import argparse
2
+ import base64
3
+ import importlib
4
+ import os.path
5
+ import sys
6
+
7
+ import requests
8
+ from datetime import datetime
9
+ from typing import List, Optional, Any, Dict
10
+ from uuid import UUID
11
+ import getpass
12
+ import socket
13
+ # import json
14
+
15
+ from ul_py_tool.commands.cmd import Cmd
16
+ from ul_py_tool.utils.colors import FG_GREEN, NC
17
+ from ul_py_tool.utils.write_stdout import write_stdout
18
+ from yaml import dump
19
+
20
+ from ul_api_utils.access import PermissionRegistry
21
+ from ul_api_utils.modules.api_sdk_jwt import ApiSdkJwt, ALGORITHMS
22
+ # from ul_api_utils.utils.json_encoder import CustomJSONEncoder
23
+
24
+
25
+ class CmdEncKeys(Cmd):
26
+ algo: str
27
+ name: str
28
+ services: List[str]
29
+ dir: str
30
+ permissions_module: Optional[str] = None
31
+ permissions_uri: Optional[str] = None
32
+ env: Optional[str] = None
33
+ org_id: Optional[UUID] = None
34
+ user_id: Optional[UUID] = None
35
+
36
+ @staticmethod
37
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
38
+ parser.add_argument('--algorithm', dest='algo', type=str, choices=ALGORITHMS, required=True)
39
+ parser.add_argument('--service-name', dest='name', type=str, required=True)
40
+ parser.add_argument('--follower-services', dest='services', nargs='*', required=False, default=[], type=str)
41
+ parser.add_argument('--result-dir', dest='dir', type=str, required=False, default=os.path.join(os.getcwd(), '.tmp', f'enc-keys-{datetime.now().date().isoformat()}'))
42
+ parser.add_argument('--jwt-permissions-module', dest='permissions_module', type=str, required=False, default=None)
43
+ parser.add_argument('--jwt-permissions-uri', dest='permissions_uri', type=str, required=False, default=None)
44
+ parser.add_argument('--jwt-environment', dest='env', type=str, required=False, default=None)
45
+ parser.add_argument('--jwt-user-id', dest='user_id', type=UUID, required=False, default=None)
46
+ parser.add_argument('--jwt-organization-id', dest='org_id', type=UUID, required=False, default=None)
47
+
48
+ def run(self) -> None:
49
+ write_stdout('')
50
+ os.makedirs(self.dir, exist_ok=True)
51
+
52
+ services = set(self.services)
53
+
54
+ private_key, pub_key_factory = ApiSdkJwt.generate_cert(self.algo) # type: ignore
55
+ service_pub_key = pub_key_factory()
56
+
57
+ structure: Dict[str, Any] = dict()
58
+
59
+ structure['info'] = dict()
60
+ structure['info']['generated_at'] = datetime.now().isoformat()
61
+ structure['info']['generated_user'] = getpass.getuser()
62
+ structure['info']['generated_hostname'] = socket.gethostname()
63
+ structure['info']['generated_algorithm'] = self.algo
64
+ if self.env:
65
+ structure['info']['environment'] = self.env
66
+
67
+ structure['service'] = dict()
68
+ structure['service'][self.name] = dict()
69
+ structure['service'][self.name]['public_key'] = base64.b64encode(service_pub_key.encode('utf-8')).decode('utf-8')
70
+ structure['service'][self.name]['private_key'] = base64.b64encode(private_key.encode('utf-8')).decode('utf-8')
71
+
72
+ structure['follower_services'] = dict()
73
+ for service in services:
74
+ pub_key = pub_key_factory().encode('utf-8')
75
+ structure['follower_services'][service] = dict()
76
+ structure['follower_services'][service]['public_key'] = base64.b64encode(pub_key).decode('utf-8')
77
+ # structure['follower_services'][service]['private_key'] = ""
78
+
79
+ if self.permissions_uri:
80
+ assert self.permissions_uri
81
+ assert self.algo
82
+ assert self.env
83
+ assert self.user_id
84
+ permissions_response = requests.get(self.permissions_uri)
85
+ assert permissions_response.status_code == 200, f'permissions requests faild. {permissions_response.status_code} :: {permissions_response.json()}'
86
+ permissions_json = permissions_response.json()
87
+ assert 'payload' in permissions_json
88
+ permissions_payload: List[Dict[str, str | int]] = permissions_json['payload']
89
+
90
+ permissions_list: List[int] = []
91
+ for permissions_dict in permissions_payload:
92
+ permissions_list.extend([p['id'] for p in permissions_dict.get('permissions')]) # type: ignore
93
+
94
+ jwt_data = dict(
95
+ environment=self.env,
96
+ user_id=self.user_id,
97
+ organization_id=self.org_id,
98
+ permissions=permissions_list,
99
+ access_expiration_date=datetime(2030, 1, 1),
100
+ refresh_expiration_date=datetime(2030, 1, 1),
101
+ )
102
+ att, _ = ApiSdkJwt.create_jwt_pair(**jwt_data) # type: ignore
103
+ structure['service'][self.name]['full_access_jwt_token'] = att.encode(private_key, self.algo) # type: ignore
104
+ # structure['service'][self.name]['full_access_jwt_data'] = json.loads(json.dumps(jwt_data, cls=CustomJSONEncoder))
105
+
106
+ for service in self.services:
107
+ if len(service.split(':')) > 1:
108
+ service_name, service_user_id = service.split(':')
109
+ try:
110
+ UUID(service_user_id)
111
+ except ValueError:
112
+ raise ValueError(f"invlid user_id type must be UUID hex for follower service - {service_name}")
113
+ jwt_data['user_id'] = service_user_id
114
+ att, _rtt = ApiSdkJwt.create_jwt_pair(**jwt_data) # type: ignore
115
+ structure['follower_services'][service]['full_access_jwt_token'] = att.encode(private_key, self.algo) # type: ignore
116
+ # structure['service'][service]['full_access_jwt_data'] = json.loads(json.dumps(jwt_data, cls=CustomJSONEncoder))
117
+
118
+ elif self.permissions_module:
119
+ assert self.permissions_module
120
+ assert self.algo
121
+ assert self.env
122
+ assert self.user_id
123
+ sys.path.append(os.getcwd())
124
+ mdl = importlib.import_module(self.permissions_module)
125
+ permissions = None
126
+ for k in dir(mdl):
127
+ v = getattr(mdl, k)
128
+ if isinstance(v, PermissionRegistry):
129
+ permissions = v
130
+ break
131
+ assert permissions is not None
132
+
133
+ jwt_data = dict(
134
+ environment=self.env,
135
+ user_id=self.user_id,
136
+ organization_id=self.org_id,
137
+ permissions=permissions.get_permissions_ids(),
138
+ access_expiration_date=datetime(2030, 1, 1),
139
+ refresh_expiration_date=datetime(2030, 1, 1),
140
+ )
141
+
142
+ att, _ = ApiSdkJwt.create_jwt_pair(**jwt_data) # type: ignore
143
+ structure['service'][self.name]['full_access_jwt_token'] = att.encode(private_key, self.algo) # type: ignore
144
+ # structure['service'][self.name]['full_access_jwt_data'] = json.loads(json.dumps(jwt_data, cls=CustomJSONEncoder))
145
+
146
+ for service in self.services:
147
+ if len(service.split(':')) > 1:
148
+ service_name, service_user_id = service.split(':')
149
+ try:
150
+ UUID(service_user_id)
151
+ except ValueError:
152
+ raise ValueError(f"invlid user_id type must be UUID hex for follower service - {service_name}")
153
+ jwt_data['user_id'] = service_user_id
154
+ att, _rtt = ApiSdkJwt.create_jwt_pair(**jwt_data) # type: ignore
155
+ structure['follower_services'][service]['full_access_jwt_token'] = att.encode(private_key, self.algo) # type: ignore
156
+ # structure['follower_services'][service]['full_access_jwt_data'] = json.loads(json.dumps(jwt_data, cls=CustomJSONEncoder))
157
+
158
+ # with open(os.path.join(self.dir, f'{self.env.upper()}__{self.name}__{self.algo}.private_key.pem'), 'w') as f:
159
+ # f.writelines(private_key.splitlines(keepends=True))
160
+ # write_stdout(f' {FG_GREEN}saved:{NC} {os.path.relpath(f.name, os.getcwd())}')
161
+
162
+ if self.env:
163
+ result_file_name = f'{self.name}__{self.env.upper()}__{self.algo}__{datetime.now().date()}__encryption_keys.yml'
164
+ else:
165
+ result_file_name = f'{self.name}__{self.algo}__{datetime.now().date()}__encryption_keys.yml'
166
+
167
+ with open(os.path.join(self.dir, result_file_name), 'w') as f:
168
+ dump(structure, f, sort_keys=False)
169
+ write_stdout(f' {FG_GREEN}saved:{NC} {os.path.relpath(f.name, os.getcwd())}')
170
+
171
+ write_stdout('')
172
+ write_stdout(f'{FG_GREEN}DONE{NC}')
@@ -0,0 +1,77 @@
1
+ import argparse
2
+ import logging
3
+ import os.path
4
+ from uuid import UUID
5
+
6
+ from requests.exceptions import HTTPError
7
+
8
+ import requests
9
+ from datetime import datetime
10
+ from typing import Any, Dict
11
+
12
+
13
+ from ul_py_tool.commands.cmd import Cmd
14
+ from ul_py_tool.utils.colors import FG_GREEN, NC
15
+ from ul_py_tool.utils.write_stdout import write_stdout
16
+ from yaml import dump
17
+
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def short_date_validator(value: str) -> datetime:
23
+ try:
24
+ return datetime.combine(datetime.strptime(value, "%Y-%m-%d"), datetime.max.time())
25
+ except ValueError:
26
+ msg = "not a valid date: {0!r}".format(value)
27
+ raise argparse.ArgumentTypeError(msg)
28
+
29
+
30
+ class CmdGenerateApiUserToken(Cmd):
31
+ uri_auth_api: str
32
+ internal_access_key: str
33
+ api_user_id: UUID
34
+ dir: str
35
+
36
+ @staticmethod
37
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
38
+ parser.add_argument('--uri', dest='uri_auth_api', type=str, required=True)
39
+ parser.add_argument('--key', dest='internal_access_key', type=str, required=True)
40
+ parser.add_argument('--id', dest='api_user_id', type=UUID, required=True)
41
+ parser.add_argument('--result-dir', dest='dir', type=str, required=False, default=os.path.join(os.getcwd(), '.tmp', f'api-user-tokens-{datetime.now().date().isoformat()}'))
42
+
43
+ def run(self) -> None:
44
+ write_stdout('')
45
+ os.makedirs(self.dir, exist_ok=True)
46
+
47
+ structure: Dict[str, Any] = dict()
48
+
49
+ api_user_headers = {
50
+ 'Authorization': f'Bearer {self.internal_access_key}',
51
+ }
52
+
53
+ try:
54
+ new_api_user_token_request = requests.post(
55
+ url=f"{self.uri_auth_api}/api/v1/tokens/{self.api_user_id}",
56
+ headers=api_user_headers,
57
+ )
58
+ new_api_user_token_request.raise_for_status()
59
+ new_api_user_token = new_api_user_token_request.json()['payload']
60
+ except HTTPError as e:
61
+ logger.info(new_api_user_token_request.text)
62
+ logger.error(f'request for create api user failed :: {new_api_user_token_request.status_code} status code :: {e}')
63
+ raise
64
+
65
+ structure['meta'] = {}
66
+ structure['meta']['uri'] = self.uri_auth_api
67
+ structure[str(self.api_user_id)] = {}
68
+ structure[str(self.api_user_id)]['token'] = new_api_user_token['access_token']
69
+
70
+ result_file_name = f'{self.api_user_id}__{datetime.now().date()}__api_user_token.yml'
71
+
72
+ with open(os.path.join(self.dir, result_file_name), 'w') as f:
73
+ dump(structure, f, sort_keys=False)
74
+ write_stdout(f' {FG_GREEN}saved:{NC} {os.path.relpath(f.name, os.getcwd())}')
75
+
76
+ write_stdout('')
77
+ write_stdout(f'{FG_GREEN}DONE{NC}')
@@ -0,0 +1,106 @@
1
+ import argparse
2
+ import logging
3
+ import os.path
4
+ from requests.exceptions import HTTPError
5
+
6
+ import requests
7
+ from datetime import datetime
8
+ from typing import List, Optional, Any, Dict
9
+
10
+
11
+ from ul_py_tool.commands.cmd import Cmd
12
+ from ul_py_tool.utils.colors import FG_GREEN, NC
13
+ from ul_py_tool.utils.write_stdout import write_stdout
14
+ from yaml import dump
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def short_date_validator(value: str) -> datetime:
21
+ try:
22
+ return datetime.combine(datetime.strptime(value, "%Y-%m-%d"), datetime.max.time())
23
+ except ValueError:
24
+ msg = "not a valid date: {0!r}".format(value)
25
+ raise argparse.ArgumentTypeError(msg)
26
+
27
+
28
+ class CmdGenerateNewApiUser(Cmd):
29
+ uri_auth_api: str
30
+ internal_access_key: str
31
+ permissions_list: List[int]
32
+ permissions_uri: Optional[str] = None
33
+ api_user_name: str
34
+ api_user_note: str
35
+ api_user_date_exp: datetime
36
+ dir: str
37
+
38
+ @staticmethod
39
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
40
+ parser.add_argument('--uri', dest='uri_auth_api', type=str, required=True)
41
+ parser.add_argument('--key', dest='internal_access_key', type=str, required=True)
42
+ parser.add_argument('--name', dest='api_user_name', type=str, required=True)
43
+ parser.add_argument('--note', dest='api_user_note', type=str, required=True)
44
+ parser.add_argument('--exp-dt', dest='api_user_date_exp', type=short_date_validator, required=True)
45
+ parser.add_argument('--permissions', dest='permissions_list', nargs='*', required=False, default=[], type=int)
46
+ parser.add_argument('--permissions-uri', dest='permissions_uri', type=str, required=False, default=None)
47
+ parser.add_argument('--result-dir', dest='dir', type=str, required=False, default=os.path.join(os.getcwd(), '.tmp', f'api-user-tokens-{datetime.now().date().isoformat()}'))
48
+
49
+ def run(self) -> None:
50
+ write_stdout('')
51
+ os.makedirs(self.dir, exist_ok=True)
52
+
53
+ structure: Dict[str, Any] = dict()
54
+
55
+ # generate permissions list
56
+ permissions_list: List[int] = self.permissions_list
57
+ if self.permissions_uri and not permissions_list:
58
+ permissions_response = requests.get(self.permissions_uri)
59
+ assert permissions_response.status_code == 200, f'permissions requests faild. {permissions_response.status_code} :: {permissions_response.json()}'
60
+ permissions_json = permissions_response.json()
61
+ assert 'payload' in permissions_json
62
+ permissions_payload: List[Dict[str, str | int]] = permissions_json['payload']
63
+
64
+ for permissions_dict in permissions_payload:
65
+ permissions_list.extend([p['id'] for p in permissions_dict.get('permissions')]) # type: ignore
66
+
67
+ api_user_payload = {
68
+ 'name': self.api_user_name,
69
+ 'note': self.api_user_note,
70
+ 'permissions': permissions_list,
71
+ 'date_expiration': self.api_user_date_exp.isoformat(),
72
+ }
73
+ api_user_headers = {
74
+ 'Authorization': f'Bearer {self.internal_access_key}',
75
+ }
76
+
77
+ try:
78
+ new_api_user_request = requests.post(
79
+ url=f"{self.uri_auth_api}/api/v1/tokens",
80
+ json=api_user_payload,
81
+ headers=api_user_headers,
82
+ )
83
+ new_api_user_request.raise_for_status()
84
+ new_api_user_data = new_api_user_request.json()['payload']
85
+ except HTTPError as e:
86
+ logger.info(new_api_user_request.json())
87
+ logger.error(f'request for create api user failed :: {new_api_user_request.status_code} status code :: {e}')
88
+ raise
89
+ structure['meta'] = {}
90
+ structure['meta']['uri'] = self.uri_auth_api
91
+ structure['meta']['name'] = self.api_user_name
92
+ structure['meta']['note'] = self.api_user_note
93
+ structure['meta']['permissions'] = permissions_list
94
+ structure['meta']['date_expiration'] = self.api_user_date_exp
95
+ structure[self.api_user_name] = {}
96
+ structure[self.api_user_name]['id'] = new_api_user_data['id']
97
+ structure[self.api_user_name]['token'] = new_api_user_data['access_token']
98
+
99
+ result_file_name = f'{self.api_user_name}__{datetime.now().date()}__api_user_data.yml'
100
+
101
+ with open(os.path.join(self.dir, result_file_name), 'w') as f:
102
+ dump(structure, f, sort_keys=False)
103
+ write_stdout(f' {FG_GREEN}saved:{NC} {os.path.relpath(f.name, os.getcwd())}')
104
+
105
+ write_stdout('')
106
+ write_stdout(f'{FG_GREEN}DONE{NC}')
@@ -0,0 +1,181 @@
1
+ import argparse
2
+ import ast
3
+ import csv
4
+ import importlib
5
+ import inspect
6
+ import os
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import Dict, Callable, Any, List, TextIO
10
+ from flask import url_for, Flask
11
+ from ul_py_tool.commands.cmd import Cmd
12
+
13
+ from ul_api_utils import conf
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class CmdGenApiFunctionDocumentation(Cmd):
19
+ api_dir: str
20
+ db_dir: str
21
+ include_api_utils_doc: bool
22
+ include_db_utils_doc: bool
23
+ app_host: str
24
+
25
+ @staticmethod
26
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
27
+ parser.add_argument('--api-dir', dest='api_dir', type=str, required=True)
28
+ parser.add_argument('--db-dir', dest='db_dir', type=str, required=True)
29
+ parser.add_argument('--app-host', dest='app_host', type=str, required=False, default="{baseUrl}")
30
+ parser.add_argument('--include-utils-api', dest='include_api_utils_doc', type=bool, required=False, default=False)
31
+ parser.add_argument('--include-utils-db', dest='include_db_utils_doc', type=bool, required=False, default=False)
32
+
33
+ @property
34
+ def api_module(self) -> str:
35
+ return self.api_dir.replace('/', '.')
36
+
37
+ @property
38
+ def api_main_module(self) -> str:
39
+ return self.api_dir.replace('/', '.') + ".main"
40
+
41
+ def run(self) -> None:
42
+ root_folder = os.getcwd()
43
+ conf.APPLICATION_DIR = os.path.join(root_folder, self.api_dir) # because sdk uses this variable to load routes
44
+ current_app = self.load_flask_app(self.api_main_module)
45
+ api_utils_functions = self.load_functions(f'{self.api_dir}/utils')
46
+ conf.APPLICATION_DIR = os.path.join(root_folder, self.db_dir)
47
+ db_helper_functions = self.load_functions(f'{self.db_dir}/models_manager')
48
+ db_utils_functions = self.load_functions(f"{self.db_dir}/utils")
49
+ with current_app.app_context():
50
+ current_app.config['SERVER_NAME'] = self.app_host
51
+ csv_data: List[Dict[str, str]] = []
52
+ with current_app.app_context():
53
+ now = datetime.now().isoformat()
54
+ filename = f'.tmp/{now}-doc.md'
55
+ with open(filename, 'w') as file:
56
+ for api_route_id, flask_api_rule in enumerate(current_app.url_map.iter_rules()):
57
+ options = {}
58
+ for arg in flask_api_rule.arguments:
59
+ options[arg] = "[{0}]".format(arg)
60
+ api_route_methods = ','.join([method for method in flask_api_rule.methods if method not in ('HEAD', 'OPTIONS')]) # type: ignore
61
+ api_route_path = url_for(flask_api_rule.endpoint, **options).replace('%5B', '[').replace('%5D', ']')
62
+ func_object = current_app.view_functions[flask_api_rule.endpoint]
63
+ if not func_object.__module__.startswith(self.api_module):
64
+ continue
65
+ csv_data.append({
66
+ "api_path": api_route_path,
67
+ "api_methods": api_route_methods,
68
+ "api_function_name": func_object.__name__,
69
+ })
70
+ self.generate_documentation(
71
+ func_object,
72
+ file,
73
+ api_route_id=api_route_id,
74
+ api_route_path=api_route_path,
75
+ api_route_methods=api_route_methods,
76
+ loaded_db_helper_functions=db_helper_functions,
77
+ loaded_api_utils_functions=api_utils_functions,
78
+ loaded_db_utils_functions=db_utils_functions,
79
+ )
80
+ with open(f'.tmp/{now}-doc.csv', 'w') as csvfile:
81
+ fields = ['api_path', 'api_methods', 'api_function_name']
82
+ writer = csv.DictWriter(csvfile, fieldnames=fields)
83
+ writer.writeheader()
84
+ writer.writerows(csv_data)
85
+
86
+ @staticmethod
87
+ def load_functions(directory: str) -> Dict[str, Callable[..., Any]]:
88
+ function_name_object__map: dict[str, Callable[..., Any]] = {}
89
+ for root, _dirs, files in os.walk(directory):
90
+ for file in files:
91
+ py_postfix = '.py'
92
+ if file.endswith(py_postfix):
93
+ module_name = file[:-len(py_postfix)]
94
+ module_path = os.path.join(root, file)
95
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
96
+ assert spec is not None # only for mypy
97
+ assert spec.loader is not None # only for mypy
98
+ module = importlib.util.module_from_spec(spec)
99
+ spec.loader.exec_module(module)
100
+ functions = inspect.getmembers(module, inspect.isfunction)
101
+ for name, func in functions:
102
+ function_name_object__map[name] = func
103
+ return function_name_object__map
104
+
105
+ @staticmethod
106
+ def load_flask_app(api_sdk_module: str) -> Flask:
107
+ module = importlib.import_module(api_sdk_module)
108
+ return module.flask_app
109
+
110
+ @staticmethod
111
+ def find_called_functions_in_api(api_function_object: Callable[..., Any]) -> List[Any]:
112
+ calls = []
113
+ source = inspect.getsource(api_function_object)
114
+ tree = ast.parse(source)
115
+ for node in ast.walk(tree):
116
+ if isinstance(node, ast.Call):
117
+ if isinstance(node.func, ast.Name):
118
+ calls.append(node.func.id)
119
+ elif isinstance(node.func, ast.Attribute):
120
+ calls.append(node.func.attr)
121
+ else:
122
+ continue
123
+ return calls
124
+
125
+ def generate_documentation(
126
+ self,
127
+ func_object: Callable[..., Any],
128
+ file_object: TextIO,
129
+ *,
130
+ api_route_id: int,
131
+ api_route_path: str,
132
+ api_route_methods: str,
133
+ loaded_db_helper_functions: Dict[str, Callable[..., Any]],
134
+ loaded_db_utils_functions: Dict[str, Callable[..., Any]],
135
+ loaded_api_utils_functions: Dict[str, Callable[..., Any]],
136
+ ) -> None:
137
+ func_name = func_object.__name__
138
+ functions_called_in_api_route = self.find_called_functions_in_api(func_object)
139
+ docstring = inspect.getdoc(func_object)
140
+ api_docstring = 'None' if docstring is None else docstring
141
+
142
+ file_object.write(f"## {api_route_id} Путь апи {api_route_path}\n\n")
143
+ file_object.write(f"#### Имя функции апи: {func_name}\n")
144
+ file_object.write(f"### Апи методы: {api_route_methods}\n\n")
145
+ file_object.write("**Описание апи метода:** \n\n")
146
+ file_object.write(f"```python\n{api_docstring}\n```\n")
147
+ helper_call = 1
148
+ for function_called_in_api_route in functions_called_in_api_route:
149
+ if function_called_in_api_route not in ('transaction_commit', 'and_', 'or_', 'foreign', 'query_soft_delete', 'ensure_db_object_exists', 'db_search'):
150
+ if function_called_in_api_route in loaded_db_helper_functions:
151
+ helper_func_obj = loaded_db_helper_functions[function_called_in_api_route]
152
+ helper_docstring = inspect.getdoc(helper_func_obj)
153
+ helper_docstring = 'None' if helper_docstring is None else helper_docstring
154
+
155
+ file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
156
+ file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
157
+ file_object.write(f"```python\n{helper_docstring}\n```\n")
158
+ helper_call += 1
159
+ elif function_called_in_api_route in loaded_api_utils_functions:
160
+ if self.include_api_utils_doc:
161
+ util_func_obj = loaded_api_utils_functions[function_called_in_api_route]
162
+ util_docstring = inspect.getdoc(util_func_obj)
163
+ util_docstring = 'None' if util_docstring is None else util_docstring
164
+ if 'db_tables_used' in util_docstring or 'db_table_used' in util_docstring:
165
+ file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
166
+ file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
167
+ file_object.write(f"```python\n{util_docstring}\n```\n")
168
+ helper_call += 1
169
+ elif function_called_in_api_route in loaded_db_utils_functions:
170
+ if self.include_db_utils_doc:
171
+ util_func_obj = loaded_db_utils_functions[function_called_in_api_route]
172
+ db_util_docstring = inspect.getdoc(util_func_obj)
173
+ db_util_docstring = 'None' if db_util_docstring is None else db_util_docstring
174
+ if 'db_tables_used' in db_util_docstring or 'db_table_used' in db_util_docstring:
175
+ file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
176
+ file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
177
+ file_object.write(f"```python\n{db_util_docstring}\n```\n")
178
+ helper_call += 1
179
+ file_object.write('-' * 20)
180
+ file_object.write('\n\n')
181
+ file_object.write('\n\n')