proto-builder 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of proto-builder might be problematic. Click here for more details.

@@ -0,0 +1,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: proto-builder
3
+ Version: 1.0.0
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "proto-builder"
3
+ version = "1.0.0"
4
+ dependencies = []
5
+
6
+ [build-system]
7
+ requires = ["setuptools>=61"]
8
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,358 @@
1
+ from .utils import BUILTIN_MODULES, COLLECTIONS, NONE_TYPE, PROTO, ProtoConfig, ProtoType
2
+ from .tree_structure import Node, VariantMode
3
+ from enum import Enum
4
+ import inspect
5
+ import types
6
+ from typing import (
7
+ get_args,
8
+ get_origin,
9
+ get_type_hints,
10
+ Union,
11
+ )
12
+
13
+
14
+ class BaseBuilder:
15
+
16
+ def __init__(
17
+ self,
18
+ config: ProtoConfig | None = None,
19
+ ):
20
+
21
+ self.config = config or ProtoConfig()
22
+
23
+ def format_proto_field(
24
+ self,
25
+ proto: ProtoType,
26
+ index: int,
27
+ ) -> str:
28
+
29
+ if proto.optional:
30
+ type_decl = f"optional {proto.p_type}"
31
+ elif proto.repeated:
32
+ type_decl = f"repeated {proto.p_type}"
33
+ elif proto.p_type:
34
+ type_decl = proto.p_type
35
+ else:
36
+ type_decl = None
37
+
38
+ if type_decl:
39
+ return f" {type_decl} {proto.name} = {index};"
40
+ return f" {proto.name} = {index};"
41
+
42
+ def is_enum(
43
+ self,
44
+ f_type: type,
45
+ ) -> bool:
46
+
47
+ return inspect.isclass(f_type) and issubclass(f_type, Enum)
48
+
49
+ def is_union(
50
+ self,
51
+ f_type: type,
52
+ ) -> bool:
53
+
54
+ return f_type in (types.UnionType, Union)
55
+
56
+ def is_custom(
57
+ self,
58
+ f_type: type,
59
+ ) -> bool:
60
+
61
+ return (
62
+ inspect.isclass(f_type)
63
+ and f_type not in PROTO
64
+ and not self.is_enum(f_type)
65
+ and f_type.__module__ not in BUILTIN_MODULES
66
+ )
67
+
68
+ def get_class_fields(
69
+ self,
70
+ cls: type,
71
+ ) -> dict:
72
+
73
+ return get_type_hints(cls, include_extras=True)
74
+
75
+ def enum_params(
76
+ self,
77
+ enum_class: type[Enum],
78
+ ) -> list[str]:
79
+
80
+ return [item.name for item in enum_class]
81
+
82
+ def is_collection(
83
+ self,
84
+ f_type: type,
85
+ ) -> bool:
86
+
87
+ return f_type in COLLECTIONS or get_origin(f_type) in COLLECTIONS
88
+
89
+ def is_nested_collection(
90
+ self,
91
+ f_type: type,
92
+ ) -> bool:
93
+
94
+ origin = get_origin(f_type)
95
+ args = get_args(f_type)
96
+
97
+ if f_type in COLLECTIONS:
98
+ return False
99
+
100
+ if origin in (list, tuple, set):
101
+ return bool(args) and self.is_collection(args[0])
102
+
103
+ if origin is dict:
104
+ if len(args) != 2:
105
+ return True
106
+ key_type, value_type = args
107
+ return self.is_collection(key_type) or self.is_collection(value_type)
108
+
109
+ return False
110
+
111
+ # def collect_custom_types(
112
+ # self,
113
+ # f_type: type,
114
+ # ):
115
+ # result = []
116
+ # stack = [f_type]
117
+
118
+ # while stack:
119
+ # current = stack.pop()
120
+ # origin = get_origin(current)
121
+ # args = get_args(current)
122
+
123
+ # if self.is_union(origin):
124
+ # stack.extend(arg for arg in args if arg is not NONE_TYPE)
125
+ # continue
126
+
127
+ # if self.is_collection(origin):
128
+ # stack.extend(arg for arg in args if arg is not NONE_TYPE)
129
+ # continue
130
+
131
+ # if self.is_custom(origin) or self.is_enum(origin):
132
+ # result.append(origin)
133
+ # continue
134
+
135
+ # if self.is_custom(current) or self.is_enum(current):
136
+ # result.append(current)
137
+
138
+ # return result
139
+
140
+ def collect_custom_types(
141
+ self,
142
+ f_type: type,
143
+ ):
144
+ result = []
145
+ stack = [f_type]
146
+
147
+ while stack:
148
+ current = stack.pop()
149
+ origin = get_origin(current)
150
+ args = get_args(current)
151
+
152
+ if self.is_union(origin):
153
+ stack.extend(arg for arg in reversed(args) if arg is not NONE_TYPE)
154
+ continue
155
+
156
+ if self.is_collection(origin):
157
+ stack.extend(arg for arg in reversed(args) if arg is not NONE_TYPE)
158
+ if origin not in result:
159
+ result.append(origin)
160
+ continue
161
+
162
+ if current not in result:
163
+ result.append(current)
164
+
165
+ return result
166
+
167
+ def create_path_str(
168
+ self,
169
+ *args: Node | str,
170
+ ) -> str:
171
+
172
+ return ".".join(
173
+ n.name if isinstance(n, Node) else n
174
+ for n in args
175
+ if isinstance(n, (Node, str))
176
+ )
177
+
178
+ def _variant_paths(
179
+ self,
180
+ node: Node,
181
+ *,
182
+ exact: bool,
183
+ mode: VariantMode = "include-self",
184
+ ) -> list[str]:
185
+
186
+ return [
187
+ self.create_path_str(*variant)
188
+ for variant in node.path_variants_to_root(mode=mode)
189
+ ]
190
+
191
+ def _arg_names(
192
+ self,
193
+ node: Node,
194
+ ) -> tuple[str]:
195
+
196
+ custom_args = self.collect_custom_types(node.data.get("field_type"))
197
+ return tuple(f_type.__name__ for f_type in custom_args if f_type is not None)
198
+
199
+ def is_removed(
200
+ self,
201
+ node: Node,
202
+ ) -> bool:
203
+
204
+ arg_names = self._arg_names(node)
205
+
206
+ # Exact
207
+ remove_exact_include_self = self.config.get_remove("exact", "include")
208
+ remove_exact_exclude_self = self.config.get_remove("exact", "exclude")
209
+
210
+ # Scope
211
+ remove_scope_include_self = self.config.get_remove("scope", "include")
212
+ remove_scope_exclude_self = self.config.get_remove("scope", "exclude")
213
+
214
+ # For absolute path like `Class1` | contains self
215
+ if any(arg in remove_exact_include_self or arg in remove_scope_include_self for arg in arg_names):
216
+ return True
217
+
218
+ for variant in node.path_variants_to_root(mode="all"):
219
+
220
+ # For relative path like Class1.Classs2, contains self
221
+ if self.create_path_str(*variant) in remove_exact_include_self:
222
+ return True
223
+
224
+ if self.create_path_str(*variant) in remove_scope_include_self:
225
+ return True
226
+
227
+
228
+ # For relative path like Class1.var, contains self
229
+ if any(self.create_path_str(*variant, arg) in remove_exact_include_self for arg in arg_names):
230
+ return True
231
+
232
+ if any(self.create_path_str(*variant, arg) in remove_scope_include_self for arg in arg_names):
233
+ return True
234
+
235
+ # For relative path like Class1.Classs2, exclude self
236
+ for variant in node.path_variants_to_root(mode="exclude-self"):
237
+ if self.create_path_str(*variant) in remove_exact_exclude_self:
238
+ return True
239
+
240
+ if self.create_path_str(*variant) in remove_scope_exclude_self:
241
+ return True
242
+
243
+ return False
244
+
245
+ def is_optional(
246
+ self,
247
+ node: Node,
248
+ ) -> bool:
249
+
250
+ if self.config.optional_all:
251
+ return True
252
+
253
+ arg_names = self._arg_names(node)
254
+
255
+ # Exact
256
+ optional_exact_include_self = self.config.get_optional("exact", "include")
257
+
258
+ # Scope
259
+ optional_scope_include_self = self.config.get_optional("scope", "include")
260
+ optional_scope_exclude_self = self.config.get_optional("scope", "exclude")
261
+
262
+ # For absolute path like `Class1` | contains self
263
+ if any(arg in optional_exact_include_self or arg in optional_scope_include_self for arg in arg_names):
264
+ return True
265
+
266
+ # Scope
267
+ for variant in node.path_variants_to_root(mode="all"):
268
+
269
+ # For relative path like Class1.Classs2, contains self
270
+ if self.create_path_str(*variant) in optional_scope_include_self:
271
+ return True
272
+
273
+ # For relative path like Class1.var, contains self
274
+ if any(self.create_path_str(*variant, arg) in optional_scope_include_self or arg in optional_scope_include_self for arg in arg_names):
275
+ return True
276
+
277
+ for variant in node.path_variants_to_root(mode="exclude-self"):
278
+
279
+ # Exact
280
+ # For relative path like Class1.var, contains self
281
+ if any(self.create_path_str(*variant, arg) in optional_exact_include_self for arg in arg_names):
282
+ return True
283
+
284
+ # Scope
285
+ # For relative path like Class1.Classs2, exclude self
286
+ if self.create_path_str(*variant) in optional_scope_exclude_self:
287
+ return True
288
+
289
+ # Exact
290
+ for variant in node.path_variants_to_root(tags={"class"}, tag_mode="include", mode="include-self"):
291
+
292
+ # For relative path like Class1.Classs2, contains self
293
+ if self.create_path_str(*variant) in optional_exact_include_self:
294
+ return True
295
+
296
+ return False
297
+
298
+ def resolve_type(
299
+ self,
300
+ node: Node,
301
+ f_type: type,
302
+ ) -> type:
303
+
304
+ arg_names = self._arg_names(node)
305
+
306
+ # Exact
307
+ override_exact_include_self = self.config.get_override("exact", "include")
308
+
309
+ # Scope
310
+ override_scope_include_self = self.config.get_override("scope", "include")
311
+ override_scope_exclude_self = self.config.get_override("scope", "exclude")
312
+
313
+ # For absolute path like `Class1`, contains self
314
+ for arg in arg_names:
315
+
316
+ if arg in override_exact_include_self:
317
+ return override_exact_include_self.get(arg).type
318
+
319
+ if arg in override_scope_include_self:
320
+ return override_scope_include_self.get(arg).type
321
+
322
+ # Scope
323
+ for variant in node.path_variants_to_root(mode="all"):
324
+
325
+ # For relative path like Class1.Classs2, contains self
326
+ path = self.create_path_str(*variant)
327
+ if path in override_scope_include_self:
328
+ return override_scope_include_self.get(path).type
329
+
330
+ for arg in arg_names:
331
+ path = self.create_path_str(*variant, arg)
332
+ if path in override_scope_include_self:
333
+ return override_scope_include_self.get(path).type
334
+
335
+ for variant in node.path_variants_to_root(mode="exclude-self"):
336
+
337
+ # Exact
338
+ # For relative path like Class1.var, contains self
339
+ for arg in arg_names:
340
+ path = self.create_path_str(*variant, arg)
341
+ if path in override_exact_include_self:
342
+ return override_exact_include_self.get(path).type
343
+
344
+ # Scope
345
+ # For relative path like Class1.Classs2, exclude self
346
+ path = self.create_path_str(*variant)
347
+ if path in override_scope_exclude_self:
348
+ return override_scope_exclude_self.get(path).type
349
+
350
+ # Exact
351
+ for variant in node.path_variants_to_root(tags={"class"}, tag_mode="include", mode="include-self"):
352
+
353
+ # For relative path like Class1.Classs2, contains self
354
+ path = self.create_path_str(*variant)
355
+ if path in override_exact_include_self:
356
+ return override_exact_include_self.get(path).type
357
+
358
+ return f_type
@@ -0,0 +1,285 @@
1
+ from .utils import OTHER, MODULES, NONE_TYPE, ProtoConfig, Session, NodeData, Message
2
+ from .base_builder import BaseBuilder
3
+ from .session_builder import SessionBuilder
4
+ from .tree_structure import Node
5
+ import inspect
6
+ from dataclasses import asdict
7
+ from typing import (
8
+ get_origin,
9
+ get_args,
10
+ get_type_hints,
11
+ Callable,
12
+ Literal,
13
+ )
14
+
15
+ class ServiceBuilder(BaseBuilder):
16
+
17
+ def __init__(
18
+ self,
19
+ config: ProtoConfig,
20
+ ):
21
+
22
+ super().__init__(config)
23
+ self.session_builder = SessionBuilder(config)
24
+ self.trees: list[Node] = []
25
+
26
+ def should_generate_message(
27
+ self,
28
+ params: dict[str, Node],
29
+ ) -> bool:
30
+
31
+ if not params:
32
+ return False
33
+
34
+ if len(params) > 1:
35
+ return True
36
+
37
+ tree = next(iter(params.values()))
38
+ if tree.root.data.get("is_custom"):
39
+
40
+ if tree.root.data.get("message_type") == "enum":
41
+ return True
42
+
43
+ for t in self.trees:
44
+
45
+ if t.root.same_as(tree.root):
46
+ return False
47
+
48
+ if t.root.contains_subtree(tree.root):
49
+ return False
50
+
51
+ return True
52
+
53
+ def merge_model(
54
+ self,
55
+ tree: Node,
56
+ ) -> None:
57
+
58
+ if not any(
59
+ t.root.contains_subtree(tree.root)
60
+ for t in self.trees
61
+ ):
62
+ self.trees.append(tree)
63
+
64
+ def method_models(
65
+ self,
66
+ fn: Callable,
67
+ cls_name: str,
68
+ ):
69
+
70
+ sig = inspect.signature(fn)
71
+ hints = get_type_hints(fn)
72
+ method_node = Node(cls_name).root.child(fn.__name__)
73
+
74
+ def add_model(cls: type):
75
+ if cls and (self.is_custom(cls) or self.is_enum(cls)):
76
+ tree = self.session_builder.build_tree(cls)
77
+
78
+ if self.is_removed(tree.root):
79
+ return
80
+
81
+ self.merge_model(tree)
82
+
83
+ def add_type(
84
+ tp,
85
+ name: str | None = None,
86
+ ):
87
+
88
+ node = method_node.child(name or "")
89
+ resolved = self.resolve_type(node, tp)
90
+ origin = get_origin(resolved)
91
+ if self.is_union(origin):
92
+ for arg in get_args(resolved):
93
+ if arg is not NONE_TYPE:
94
+ add_model(arg)
95
+ else:
96
+ add_model(resolved)
97
+
98
+ for param in sig.parameters.values():
99
+ add_type(hints.get(param.name, param.annotation), param.name)
100
+
101
+ add_type(hints.get("return"))
102
+
103
+ def build_session(
104
+ self,
105
+ name: str,
106
+ fn: Callable,
107
+ cls_name: str,
108
+ ) -> Session:
109
+
110
+ sig = inspect.signature(fn)
111
+ hints = get_type_hints(fn)
112
+
113
+ params = {
114
+ pname: hints.get(pname, param.annotation)
115
+ for pname, param in sig.parameters.items()
116
+ }
117
+ ret = hints.get("return")
118
+
119
+ base_name = name.title().replace("_", "")
120
+ request_name = f"{base_name}Request"
121
+ response_name = f"{base_name}Response"
122
+ input_params: dict[str, Node] = {}
123
+ output_params: dict[str, Node] = {}
124
+
125
+ for param_name, param_type in params.items():
126
+ node = Node(cls_name).child(fn.__name__).child("arg").child(param_name)
127
+ node.data.update(asdict(NodeData(message_name=param_type.__name__, field_type=param_type)))
128
+
129
+ if self.is_removed(node):
130
+ continue
131
+
132
+ resolved_type = self.resolve_type(node, param_type)
133
+ input_params[param_name] = self.session_builder.build_tree(resolved_type, node)
134
+
135
+ if not input_params:
136
+ request_name = OTHER["empty"]
137
+
138
+ elif len(input_params) == 1:
139
+ name, tree = next(iter(input_params.items()))
140
+ f_type = tree.last_node().data.get("field_type")
141
+ if self.is_custom(f_type) and any(t.root.contains_subtree(tree.root) for t in self.trees):
142
+ request_name = resolved_type.__name__
143
+
144
+ if ret:
145
+
146
+ ret_origin = get_origin(ret)
147
+ ret_args = get_args(ret)
148
+ if ret_origin is tuple:
149
+ for arg in ret_args:
150
+ node = Node(cls_name).child(fn.__name__).child("return").child(arg.__name__)
151
+ node.data.update(asdict(NodeData(field_type=arg)))
152
+
153
+ if self.is_removed(node):
154
+ continue
155
+
156
+ resolved_type = self.resolve_type(node, arg)
157
+ tree = self.session_builder.build_tree(resolved_type, node)
158
+
159
+ if self.is_union(type(arg)):
160
+ output_params.update({"value": tree})
161
+ else:
162
+ output_params.update({arg.__name__.lower() + "_value": tree})
163
+ else:
164
+ node = Node(cls_name).child(fn.__name__).child("return")
165
+ resolved_type = self.resolve_type(node, ret)
166
+ tree = self.session_builder.build_tree(resolved_type, node)
167
+ output_params = {"value": tree}
168
+
169
+ if not output_params:
170
+ response_name = OTHER["empty"]
171
+
172
+ elif len(output_params) == 1:
173
+ name, tree = next(iter(output_params.items()))
174
+ f_type = tree.last_node().data.get("field_type")
175
+ if self.is_custom(f_type) and any(t.root.contains_subtree(tree.root) for t in self.trees):
176
+ response_name = resolved_type.__name__
177
+
178
+ return Session(fn.__name__, request_name, response_name, input_params, output_params)
179
+
180
+ def message_from_fields(
181
+ self,
182
+ name: str,
183
+ fields: dict[str, Node],
184
+ ) -> Message:
185
+
186
+ message: Message = Message()
187
+ lines = [f"message {name} {{"]
188
+
189
+ idx = 1
190
+ for field_name, tree in fields.items():
191
+
192
+ if self.is_removed(tree):
193
+ continue
194
+
195
+ proto = self.session_builder.proto_type(tree.last_node())
196
+ proto.name = field_name
197
+ lines.append(self.format_proto_field(proto, idx))
198
+ message.modules.append(proto.p_type)
199
+ idx += 1
200
+
201
+ lines.append("}")
202
+ if len(lines) > 2:
203
+ message.text = "\n".join(lines)
204
+
205
+ return message
206
+
207
+ def build(
208
+ self,
209
+ service_cls: type,
210
+ package_name: str = "generated",
211
+ ) -> str:
212
+
213
+ self.trees = []
214
+
215
+ cls_name = service_cls.__name__
216
+ methods = {
217
+ name: fn
218
+ for name, fn in service_cls.__dict__.items()
219
+ if inspect.isfunction(fn) and getattr(fn, "__isabstractmethod__", False)
220
+ }
221
+
222
+ for fn in methods.values():
223
+ self.method_models(fn, cls_name)
224
+
225
+ sessions = {
226
+ name: self.build_session(name, fn, cls_name)
227
+ for name, fn in methods.items()
228
+ }
229
+
230
+ header = [
231
+ 'syntax = "proto3";',
232
+ f"package {package_name};",
233
+ ]
234
+
235
+ service = [f"service {cls_name} {{"]
236
+ for name, session in sessions.items():
237
+ service.append(f" rpc {name} ({session.request_name}) returns ({session.response_name});")
238
+ service.append("}")
239
+
240
+ messages: list[str] = []
241
+ modules: list[str] = []
242
+ for tree in self.trees:
243
+ msg_list: list[Message] = self.session_builder.node_to_message(tree.root)
244
+ for msg in msg_list:
245
+ messages.append(msg.text)
246
+ modules.extend(msg.modules)
247
+
248
+ for session in sessions.values():
249
+
250
+ if self.should_generate_message(session.input_params):
251
+ msg = self.message_from_fields(session.request_name, session.input_params)
252
+ if msg.text:
253
+ messages.append(msg.text)
254
+ modules.extend(msg.modules)
255
+
256
+ if self.should_generate_message(session.output_params):
257
+ msg = self.message_from_fields(session.response_name, session.output_params)
258
+ if msg.text:
259
+ messages.append(msg.text)
260
+ modules.extend(msg.modules)
261
+
262
+ if not session.input_params or not session.output_params:
263
+ modules.append(OTHER["empty"])
264
+
265
+ imports: list[str] = []
266
+ for mdl in modules:
267
+ if mdl in MODULES:
268
+ imp = f'import "{MODULES[mdl]}";'
269
+ if imp not in imports:
270
+ imports.append(imp)
271
+
272
+ content: list[str] = []
273
+
274
+ content.append("\n".join(header))
275
+ if imports:
276
+ content.append("\n\n")
277
+ content.append("\n".join(imports))
278
+ if service:
279
+ content.append("\n\n")
280
+ content.append("\n".join(service))
281
+ if messages:
282
+ content.append("\n\n")
283
+ content.append("\n\n".join(messages))
284
+
285
+ return "".join(content)