ul-api-utils 7.7.9__py3-none-any.whl → 7.8.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.

Potentially problematic release.


This version of ul-api-utils might be problematic. Click here for more details.

@@ -5,7 +5,7 @@ import inspect
5
5
  import os
6
6
  import logging
7
7
  from datetime import datetime
8
- from typing import Dict, Callable, Any
8
+ from typing import Dict, Callable, Any, List, TextIO
9
9
 
10
10
  from flask import url_for, Flask
11
11
  from ul_py_tool.commands.cmd import Cmd
@@ -33,102 +33,133 @@ class CmdGenApiFunctionDocumentation(Cmd):
33
33
  return self.api_dir.replace('/', '.')
34
34
 
35
35
  @property
36
- def main_module(self) -> str:
36
+ def api_main_module(self) -> str:
37
37
  return self.api_dir.replace('/', '.') + ".main"
38
38
 
39
+ def run(self) -> None:
40
+ root_folder = os.getcwd()
41
+ conf.APPLICATION_DIR = os.path.join(root_folder, self.api_dir) # because sdk uses this variable to load routes
42
+ current_app = self.load_flask_app(self.api_main_module)
43
+ api_utils_functions = self.load_functions(f'{self.api_dir}/utils')
44
+ conf.APPLICATION_DIR = os.path.join(root_folder, self.db_dir)
45
+ db_helper_functions = self.load_functions(f'{self.db_dir}/models_manager')
46
+ db_utils_functions = self.load_functions(f"{self.db_dir}/utils")
47
+ with current_app.app_context():
48
+ current_app.config['SERVER_NAME'] = '{base_url}'
49
+ with current_app.app_context(), open(f'.tmp/api_doc_{datetime.now().isoformat()}.md', 'w') as file:
50
+ for api_route_id, flask_api_rule in enumerate(current_app.url_map.iter_rules()):
51
+ options = {}
52
+ for arg in flask_api_rule.arguments:
53
+ options[arg] = "[{0}]".format(arg)
54
+ api_route_methods = ','.join([method for method in flask_api_rule.methods if method not in ('HEAD', 'OPTIONS')]) # type: ignore
55
+ api_route_path = url_for(flask_api_rule.endpoint, **options).replace('%5B', '[').replace('%5D', ']')
56
+ func_object = current_app.view_functions[flask_api_rule.endpoint]
57
+ if not func_object.__module__.startswith(self.api_module):
58
+ continue
59
+ self.generate_documentation(
60
+ func_object,
61
+ file,
62
+ api_route_id=api_route_id,
63
+ api_route_path=api_route_path,
64
+ api_route_methods=api_route_methods,
65
+ loaded_db_helper_functions=db_helper_functions,
66
+ loaded_api_utils_functions=api_utils_functions,
67
+ loaded_db_utils_functions=db_utils_functions,
68
+ )
69
+
39
70
  @staticmethod
40
- def get_db_helpers_names(directory: str) -> Dict[str, Callable[..., Any]]:
41
- db_helpers = {}
71
+ def load_functions(directory: str) -> Dict[str, Callable[..., Any]]:
72
+ function_name_object__map = {}
42
73
  for root, _dirs, files in os.walk(directory):
43
74
  for file in files:
44
- if file.endswith('.py'):
45
- module_name = file[:-3] # Strip the .py to get the module name.
75
+ py_postfix = '.py'
76
+ if file.endswith(py_postfix):
77
+ module_name = file[:-len(py_postfix)]
46
78
  module_path = os.path.join(root, file)
47
79
  spec = importlib.util.spec_from_file_location(module_name, module_path)
48
- module = importlib.util.module_from_spec(spec) # type: ignore
49
- spec.loader.exec_module(module) # type: ignore
50
-
80
+ assert spec is not None # only for mypy
81
+ assert spec.loader is not None # only for mypy
82
+ module = importlib.util.module_from_spec(spec)
83
+ spec.loader.exec_module(module)
51
84
  functions = inspect.getmembers(module, inspect.isfunction)
52
85
  for name, func in functions:
53
- db_helpers[name] = func
54
- return db_helpers
86
+ function_name_object__map[name] = func
87
+ return function_name_object__map
55
88
 
56
89
  @staticmethod
57
- def get_flask_app(api_sdk_module: str) -> Flask:
90
+ def load_flask_app(api_sdk_module: str) -> Flask:
58
91
  module = importlib.import_module(api_sdk_module)
59
92
  return module.flask_app
60
93
 
61
- def run(self) -> None:
62
- conf.APPLICATION_DIR = os.path.join(os.getcwd(), self.api_dir)
63
- current_app = self.get_flask_app(self.main_module)
64
- utils = self.get_db_helpers_names(f'{self.api_dir}/utils')
65
- db_utils = self.get_db_helpers_names(f'{self.db_dir}/models_manager')
66
- db_helpers = self.get_db_helpers_names(f"{self.db_dir}/utils")
67
- with current_app.app_context():
68
- current_app.config['SERVER_NAME'] = '{base_url}'
69
- with current_app.app_context(), open(f'.tmp/api_doc_{datetime.now().isoformat()}.md', 'w') as file:
70
- for api_num, rule in enumerate(current_app.url_map.iter_rules()):
71
- options = {}
72
- for arg in rule.arguments:
73
- options[arg] = "[{0}]".format(arg)
74
- methods = ','.join([method for method in rule.methods if method not in ('HEAD', 'OPTIONS')]) # type: ignore
75
- url = url_for(rule.endpoint, **options).replace('%5B', '[').replace('%5D', ']')
76
- func_obj = current_app.view_functions[rule.endpoint]
77
- if not func_obj.__module__.startswith(self.api_module):
94
+ @staticmethod
95
+ def find_called_functions_in_api(api_function_object: Callable[..., Any]) -> List[Any]:
96
+ calls = []
97
+ source = inspect.getsource(api_function_object)
98
+ tree = ast.parse(source)
99
+ for node in ast.walk(tree):
100
+ if isinstance(node, ast.Call):
101
+ if isinstance(node.func, ast.Name):
102
+ calls.append(node.func.id)
103
+ elif isinstance(node.func, ast.Attribute):
104
+ calls.append(node.func.attr)
105
+ else:
78
106
  continue
79
- func_name = func_obj.__name__
80
- source = inspect.getsource(func_obj)
81
- tree = ast.parse(source)
82
- calls = []
83
- for node in ast.walk(tree):
84
- if isinstance(node, ast.Call):
85
- if isinstance(node.func, ast.Name):
86
- calls.append(node.func.id)
87
- elif isinstance(node.func, ast.Attribute):
88
- calls.append(node.func.attr)
89
- else:
90
- pass
107
+ return calls
91
108
 
92
- docstring = inspect.getdoc(func_obj)
93
- api_docstring = 'None' if docstring is None else docstring
109
+ def generate_documentation(
110
+ self,
111
+ func_object: Callable[..., Any],
112
+ file_object: TextIO,
113
+ *,
114
+ api_route_id: int,
115
+ api_route_path: str,
116
+ api_route_methods: str,
117
+ loaded_db_helper_functions: Dict[str, Callable[..., Any]],
118
+ loaded_db_utils_functions: Dict[str, Callable[..., Any]],
119
+ loaded_api_utils_functions: Dict[str, Callable[..., Any]],
120
+ ) -> None:
121
+ func_name = func_object.__name__
122
+ functions_called_in_api_route = self.find_called_functions_in_api(func_object)
123
+ docstring = inspect.getdoc(func_object)
124
+ api_docstring = 'None' if docstring is None else docstring
94
125
 
95
- file.write(f"## {api_num} Путь апи {url}\n\n")
96
- file.write(f"#### Имя функции апи: {func_name}\n")
97
- file.write(f"### Апи методы: {methods}\n\n")
98
- file.write("**Описание апи метода:** \n\n")
99
- file.write(f"```python\n{api_docstring}\n```\n")
100
- helper_call = 1
101
- for call in calls:
102
- if call not in ('transaction_commit', 'and_', 'or_', 'foreign', 'query_soft_delete', 'ensure_db_object_exists', 'db_search'):
103
- if call in db_helpers:
104
- helper_func_obj = db_helpers[call]
105
- helper_docstring = inspect.getdoc(helper_func_obj)
106
- helper_docstring = 'None' if helper_docstring is None else helper_docstring
126
+ file_object.write(f"## {api_route_id} Путь апи {api_route_path}\n\n")
127
+ file_object.write(f"#### Имя функции апи: {func_name}\n")
128
+ file_object.write(f"### Апи методы: {api_route_methods}\n\n")
129
+ file_object.write("**Описание апи метода:** \n\n")
130
+ file_object.write(f"```python\n{api_docstring}\n```\n")
131
+ helper_call = 1
132
+ for function_called_in_api_route in functions_called_in_api_route:
133
+ if function_called_in_api_route not in ('transaction_commit', 'and_', 'or_', 'foreign', 'query_soft_delete', 'ensure_db_object_exists', 'db_search'):
134
+ if function_called_in_api_route in loaded_db_helper_functions:
135
+ helper_func_obj = loaded_db_helper_functions[function_called_in_api_route]
136
+ helper_docstring = inspect.getdoc(helper_func_obj)
137
+ helper_docstring = 'None' if helper_docstring is None else helper_docstring
107
138
 
108
- file.write(f"### {api_num}.{helper_call} Вызвана функция работы с БД : {call}\n")
109
- file.write(f"**Описание функции {call}:**\n\n")
110
- file.write(f"```python\n{helper_docstring}\n```\n")
139
+ file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
140
+ file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
141
+ file_object.write(f"```python\n{helper_docstring}\n```\n")
142
+ helper_call += 1
143
+ elif function_called_in_api_route in loaded_api_utils_functions:
144
+ if self.include_api_utils_doc:
145
+ util_func_obj = loaded_api_utils_functions[function_called_in_api_route]
146
+ util_docstring = inspect.getdoc(util_func_obj)
147
+ util_docstring = 'None' if util_docstring is None else util_docstring
148
+ if 'db_tables_used' in util_docstring or 'db_table_used' in util_docstring:
149
+ file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
150
+ file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
151
+ file_object.write(f"```python\n{util_docstring}\n```\n")
152
+ helper_call += 1
153
+ elif function_called_in_api_route in loaded_db_utils_functions:
154
+ if self.include_db_utils_doc:
155
+ util_func_obj = loaded_db_utils_functions[function_called_in_api_route]
156
+ db_util_docstring = inspect.getdoc(util_func_obj)
157
+ db_util_docstring = 'None' if db_util_docstring is None else db_util_docstring
158
+ if 'db_tables_used' in db_util_docstring or 'db_table_used' in db_util_docstring:
159
+ file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
160
+ file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
161
+ file_object.write(f"```python\n{db_util_docstring}\n```\n")
111
162
  helper_call += 1
112
- elif call in utils:
113
- if self.include_api_utils_doc:
114
- util_func_obj = utils[call]
115
- util_docstring = inspect.getdoc(util_func_obj)
116
- util_docstring = 'None' if util_docstring is None else util_docstring
117
- if 'db_tables_used' in util_docstring or 'db_table_used' in util_docstring:
118
- file.write(f"### {api_num}.{helper_call} Вызвана функция работы с БД : {call}\n")
119
- file.write(f"**Описание функции {call}:**\n\n")
120
- file.write(f"```python\n{util_docstring}\n```\n")
121
- helper_call += 1
122
- elif call in db_utils:
123
- if self.include_db_utils_doc:
124
- util_func_obj = db_utils[call]
125
- db_util_docstring = inspect.getdoc(util_func_obj)
126
- db_util_docstring = 'None' if db_util_docstring is None else db_util_docstring
127
- if 'db_tables_used' in db_util_docstring or 'db_table_used' in db_util_docstring:
128
- file.write(f"### {api_num}.{helper_call} Вызвана функция работы с БД : {call}\n")
129
- file.write(f"**Описание функции {call}:**\n\n")
130
- file.write(f"```python\n{db_util_docstring}\n```\n")
131
- helper_call += 1
132
- file.write('-' * 20)
133
- file.write('\n\n')
134
- file.write('\n\n')
163
+ file_object.write('-' * 20)
164
+ file_object.write('\n\n')
165
+ file_object.write('\n\n')
File without changes
@@ -0,0 +1,5 @@
1
+ from wtforms import SelectMultipleField, widgets # type: ignore
2
+
3
+
4
+ class MultiCheckboxField(SelectMultipleField):
5
+ option_widget = widgets.CheckboxInput()
@@ -0,0 +1,86 @@
1
+ from typing import Any
2
+
3
+ from markupsafe import Markup
4
+ from wtforms import SelectField # type: ignore
5
+ from wtforms.widgets.core import html_params, Select # type: ignore
6
+
7
+
8
+ class CustomLiveSearchPlaceholderSelect(Select):
9
+ """
10
+ Renders a CUSTOM select field.
11
+
12
+ If `multiple` is True, then the `size` property should be specified on
13
+ rendering to make the field useful.
14
+
15
+ The field must provide an `iter_choices()` method which the widget will
16
+ call on rendering; this method must yield tuples of
17
+ `(value, label, selected)`.
18
+ It also must provide a `has_groups()` method which tells whether choices
19
+ are divided into groups, and if they do, the field must have an
20
+ `iter_groups()` method that yields tuples of `(label, choices)`, where
21
+ `choices` is an iterable of `(value, label, selected)` tuples.
22
+ Otherwise, `selected` is False for any option field in select item group.
23
+ """
24
+ def __call__(self, field: SelectField, **kwargs: Any) -> Markup:
25
+ kwargs.setdefault("id", field.id)
26
+ if self.multiple:
27
+ kwargs["multiple"] = True
28
+ flags = getattr(field, "flags", {})
29
+ for k in dir(flags):
30
+ if k in self.validation_attrs and k not in kwargs:
31
+ kwargs[k] = getattr(flags, k)
32
+
33
+ html = ["<select data-live-search='true' data-show-subtext='true' %s>" % html_params(name=field.name, **kwargs),
34
+ "<option value='' disabled selected>Select something...</option>"]
35
+
36
+ if field.has_groups():
37
+ for group, choices in field.iter_groups():
38
+ html.append("<optgroup %s>" % html_params(label=group))
39
+ for val, label, selected in choices:
40
+ html.append(self.render_option(val, label, selected))
41
+ html.append("</optgroup>")
42
+ else:
43
+ for val, label, _ in field.iter_choices():
44
+ html.append(self.render_option(val, label, False))
45
+ html.append("</select>")
46
+ return Markup("".join(html))
47
+
48
+
49
+ class CustomLiveSearchSelect(Select):
50
+ """
51
+ Renders a CUSTOM select field.
52
+
53
+ If `multiple` is True, then the `size` property should be specified on
54
+ rendering to make the field useful.
55
+
56
+ The field must provide an `iter_choices()` method which the widget will
57
+ call on rendering; this method must yield tuples of
58
+ `(value, label, selected)`.
59
+ It also must provide a `has_groups()` method which tells whether choices
60
+ are divided into groups, and if they do, the field must have an
61
+ `iter_groups()` method that yields tuples of `(label, choices)`, where
62
+ `choices` is an iterable of `(value, label, selected)` tuples.
63
+ Otherwise, `selected` is False for any option field in select item group.
64
+ """
65
+ def __call__(self, field: SelectField, **kwargs: Any) -> Markup:
66
+ kwargs.setdefault("id", field.id)
67
+ if self.multiple:
68
+ kwargs["multiple"] = True
69
+ flags = getattr(field, "flags", {})
70
+ for k in dir(flags):
71
+ if k in self.validation_attrs and k not in kwargs:
72
+ kwargs[k] = getattr(flags, k)
73
+
74
+ html = ["<select data-live-search='true' data-show-subtext='true' %s>" % html_params(name=field.name, **kwargs)]
75
+
76
+ if field.has_groups():
77
+ for group, choices in field.iter_groups():
78
+ html.append("<optgroup %s>" % html_params(label=group))
79
+ for val, label, selected in choices:
80
+ html.append(self.render_option(val, label, selected))
81
+ html.append("</optgroup>")
82
+ else:
83
+ for val, label, selected in field.iter_choices():
84
+ html.append(self.render_option(val, label, selected))
85
+ html.append("</select>")
86
+ return Markup("".join(html))
@@ -0,0 +1,42 @@
1
+ from typing import Optional, Any
2
+
3
+ from wtforms import StringField # type: ignore
4
+ from wtforms.widgets import TextInput # type: ignore
5
+ from markupsafe import Markup
6
+
7
+
8
+ class CustomTextInput(TextInput):
9
+ """
10
+ Render a single-line text input with optional input attributes ("required", "maxlength", "minlength", "pattern").
11
+
12
+ examples: TextInputCustom(required=True, min_length=5, max_length=255, pattern=r"^(d+(,d+)*)?$")
13
+ TextInputCustom(max_length=255, pattern=r"^(d+(,d+)*)?$")
14
+ TextInputCustom(required=True, pattern=r"^(d+(,d+)*)?$")
15
+ TextInputCustom(max_length=255)
16
+ In model usage: ... info={"label": "LABEL:", "widget": TextInputCustom(pattern=r"^(d+(,d+)*)?$")}
17
+ """
18
+ validation_attrs = ["required", "maxlength", "minlength", "pattern"]
19
+
20
+ def __init__(self, required: Optional[bool] = None, max_length: Optional[int] = None, min_length: Optional[int] = None, pattern: Optional[str] = None):
21
+ super().__init__()
22
+ self.required = required
23
+ self.max_length = max_length
24
+ self.min_length = min_length
25
+ self.pattern = pattern
26
+
27
+ def __call__(self, field: StringField, **kwargs: Any) -> Markup:
28
+ kwargs.setdefault("id", field.id)
29
+ kwargs.setdefault("type", self.input_type)
30
+ if self.min_length:
31
+ kwargs.setdefault("minlength", self.min_length)
32
+ if self.max_length:
33
+ kwargs.setdefault("maxlength", self.max_length)
34
+ if self.pattern:
35
+ kwargs.setdefault("pattern", self.pattern)
36
+ if "value" not in kwargs:
37
+ kwargs["value"] = field._value()
38
+ flags = getattr(field, "flags", {})
39
+ for k in dir(flags):
40
+ if k in self.validation_attrs and k not in kwargs:
41
+ kwargs[k] = getattr(flags, k)
42
+ return Markup("<input %s>" % self.html_params(name=field.name, **kwargs))
@@ -0,0 +1,75 @@
1
+ from typing import Type, Any, Dict, Optional, Tuple
2
+
3
+ from ul_db_utils.modules.db import db
4
+ from sqlalchemy.dialects import postgresql
5
+ from sqlalchemy.dialects.postgresql import ARRAY
6
+ from wtforms_alchemy import ModelForm, ClassMap, FormGenerator # type: ignore
7
+ from wtforms.fields import SelectField # type: ignore
8
+
9
+ from ul_db_utils.model.base_model import BaseModel
10
+
11
+ from ul_api_utils.resources.web_forms.custom_fields.custom_checkbox_select import MultiCheckboxField
12
+
13
+
14
+ def form_factory(model_obj: Type[BaseModel], edition: bool = False, *, extra_fields: Optional[Dict[str, Any]] = None) -> Type[ModelForm]:
15
+ """
16
+ Returns generated model form.
17
+
18
+ Parameters:
19
+ model_obj (type_of(BaseModel)): model object which will use for form generation
20
+ edition (bool): flag to indicate creation or edition form will be generated
21
+ extra_fields (optional(dict(str, any))): additional fields for generated form
22
+
23
+ Returns:
24
+ Form (type_of(ModelForm)): web model form
25
+ """
26
+
27
+ class ExtraFieldsFormGenerator(FormGenerator):
28
+ def create_fields(self, form: Any, properties: Dict[str, Any]) -> None:
29
+ """
30
+ Creates fields for given form based on given model attributes.
31
+
32
+ :param form: form to attach the generated fields into
33
+ :param properties: model attributes to generate the form fields from
34
+ """
35
+ super(ExtraFieldsFormGenerator, self).create_fields(form, properties)
36
+
37
+ if extra_fields:
38
+ for field_name, field in extra_fields.items():
39
+ setattr(form, field_name, field)
40
+
41
+ class Form(ModelForm):
42
+ """
43
+ A class for representing a web form.
44
+
45
+ ...
46
+
47
+ Attributes
48
+ ----------
49
+ property_columns : dict(str, any)
50
+ Model property columns such as ID, USER_CREATED etc.
51
+
52
+ Methods
53
+ -------
54
+
55
+ """
56
+
57
+ property_columns: Dict[str, Any] = {}
58
+
59
+ def __init__(self, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> None:
60
+ super(Form, self).__init__(*args, **kwargs)
61
+ self.property_columns = model_obj.get_property_columns(self._obj) if self._obj else {}
62
+
63
+ class Meta:
64
+ model = model_obj
65
+ type_map = ClassMap({postgresql.UUID: SelectField, ARRAY: MultiCheckboxField})
66
+ only = model_obj.get_edit_columns() if edition else model_obj.get_create_columns()
67
+ form_generator = ExtraFieldsFormGenerator
68
+
69
+ @classmethod
70
+ def get_session(cls) -> Any:
71
+ return db.session
72
+
73
+ Form.__name__ = f"{model_obj.__name__}Form"
74
+
75
+ return Form
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ul-api-utils
3
- Version: 7.7.9
3
+ Version: 7.8.0
4
4
  Summary: Python api utils
5
5
  Author: Unic-lab
6
6
  Author-email:
@@ -32,7 +32,7 @@ ul_api_utils/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
32
32
  ul_api_utils/commands/cmd_enc_keys.py,sha256=-Tblh6lI7G6M5YVwbVQqZmXhBMiIpB3a7b0Lv1MFufk,8453
33
33
  ul_api_utils/commands/cmd_gen_api_user_token.py,sha256=Vg7oEYHvof7DSLat9yJ_k5AYL9ZOC4Jvd38DBn5U-R0,2730
34
34
  ul_api_utils/commands/cmd_gen_new_api_user.py,sha256=ICZbKqz2D6DRvjwtNM08rNjIlWN3qClcUQw5L8FxRBY,4549
35
- ul_api_utils/commands/cmd_generate_api_docs.py,sha256=2knVsUnOlKbMV7KVL75h1fRxLI_715tpinF4BRjr1b4,7013
35
+ ul_api_utils/commands/cmd_generate_api_docs.py,sha256=t4wIVN6cdQLN7FX7_RWxGy0D8tUSe4bs2_HNVdLrwKk,8788
36
36
  ul_api_utils/commands/cmd_start.py,sha256=QH5hEMJCoUpI2xKqWMtcp_9CU5YgmIlizT_s9lVJGUM,3441
37
37
  ul_api_utils/commands/cmd_worker_start.py,sha256=1tt4_mL8T8_q7i1bqnfjPSkSYlRNNNp8eJ-5rTYj36w,2593
38
38
  ul_api_utils/commands/start/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -77,6 +77,13 @@ ul_api_utils/resources/health_check/const.py,sha256=QzVZP_ZKmVKleUACiOGjzP-v54FD
77
77
  ul_api_utils/resources/health_check/health_check.py,sha256=bb_ONn3akNXxNFELIUcW6foqwiKqeinmr-Wl0fQ7Q4c,18449
78
78
  ul_api_utils/resources/health_check/health_check_template.py,sha256=Qih-sVoFVoVxfmDYBTzwlNSicCr7zNelUJLJMnM-C_Q,2572
79
79
  ul_api_utils/resources/health_check/resource.py,sha256=SPd9kMzBOVhFZgMVfV26bDpZya3BmwxTfOR4MZ2dL4o,4199
80
+ ul_api_utils/resources/web_forms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
+ ul_api_utils/resources/web_forms/uni_form.py,sha256=x71Ti_Rkb0WxXq_SkwltHMYX0BYKsoV-8DPivra0HHw,2699
82
+ ul_api_utils/resources/web_forms/custom_fields/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
+ ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py,sha256=M2k9qqSXSXjd3ajGMjK3dQwCFefNKf-AL89PSOU0GxU,158
84
+ ul_api_utils/resources/web_forms/custom_widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
+ ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py,sha256=691DZl8znlcKvl03SflpZWcp_9Rs_4LvPLfjsqf2Tn4,3777
86
+ ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py,sha256=Mn6MpFrZIHPjM-l1ytWC6yGiHsIdcfbJoZiTF9XIENc,1888
80
87
  ul_api_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
88
  ul_api_utils/utils/api_encoding.py,sha256=dqXknOKgc3HBOk48zDoVOLLwc1vRFATkBY8JW_RVLCo,1450
82
89
  ul_api_utils/utils/api_format.py,sha256=wGuk5QP6b7oyl9SJ1ebNVirzCipmyTxqjoEPh2cJzAY,2025
@@ -126,9 +133,9 @@ ul_api_utils/validators/validate_empty_object.py,sha256=3Ck_iwyJE_M5e7l6s1i88aqb
126
133
  ul_api_utils/validators/validate_uuid.py,sha256=EfvlRirv2EW0Z6w3s8E8rUa9GaI8qXZkBWhnPs8NFrA,257
127
134
  ul_api_utils/validators/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
135
  ul_api_utils/validators/__tests__/test_custom_fields.py,sha256=QLZ7DFta01Z7DOK9Z5Iq4uf_CmvDkVReis-GAl_QN48,1447
129
- ul_api_utils-7.7.9.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
130
- ul_api_utils-7.7.9.dist-info/METADATA,sha256=ncYJbK4iwjikN-nF3Op017oXYMa3aiCmfnU01PizxlY,14454
131
- ul_api_utils-7.7.9.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
132
- ul_api_utils-7.7.9.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
133
- ul_api_utils-7.7.9.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
134
- ul_api_utils-7.7.9.dist-info/RECORD,,
136
+ ul_api_utils-7.8.0.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
137
+ ul_api_utils-7.8.0.dist-info/METADATA,sha256=ye4_rHtf-C06Ltam7yrnIsVqoJ5IqMFZxpIQEh4xOfg,14454
138
+ ul_api_utils-7.8.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
139
+ ul_api_utils-7.8.0.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
140
+ ul_api_utils-7.8.0.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
141
+ ul_api_utils-7.8.0.dist-info/RECORD,,