nativegate 0.1.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 (50) hide show
  1. nativegate/__init__.py +1 -0
  2. nativegate/__main__.py +4 -0
  3. nativegate/buildinfo.py +344 -0
  4. nativegate/cli.py +2007 -0
  5. nativegate/config.py +991 -0
  6. nativegate/declared_invariants.py +565 -0
  7. nativegate/discovery.py +167 -0
  8. nativegate/driverbuild.py +626 -0
  9. nativegate/drivers/__init__.py +5 -0
  10. nativegate/drivers/cpp.py +616 -0
  11. nativegate/drivers/fortran.py +507 -0
  12. nativegate/generators/__init__.py +0 -0
  13. nativegate/generators/cmake_gen.py +101 -0
  14. nativegate/generators/docker_gen.py +614 -0
  15. nativegate/generators/error_gen.py +104 -0
  16. nativegate/generators/f2py_gen.py +91 -0
  17. nativegate/generators/gateway_gen.py +110 -0
  18. nativegate/generators/golden_gen.py +50 -0
  19. nativegate/generators/k8s_gen.py +212 -0
  20. nativegate/generators/mcp_gen.py +281 -0
  21. nativegate/generators/middleware_gen.py +717 -0
  22. nativegate/generators/pybind_gen.py +406 -0
  23. nativegate/generators/pyproject_gen.py +61 -0
  24. nativegate/generators/python_pkg_gen.py +1164 -0
  25. nativegate/generators/test_gen.py +160 -0
  26. nativegate/golden.py +747 -0
  27. nativegate/invariants.py +532 -0
  28. nativegate/ir.py +789 -0
  29. nativegate/lattice.py +350 -0
  30. nativegate/locking.py +216 -0
  31. nativegate/oracle.py +904 -0
  32. nativegate/parsers/__init__.py +0 -0
  33. nativegate/parsers/cpp.py +105 -0
  34. nativegate/parsers/cpp_ast.py +1652 -0
  35. nativegate/parsers/cpp_regex.py +812 -0
  36. nativegate/parsers/fixed_form.py +868 -0
  37. nativegate/parsers/fortran.py +157 -0
  38. nativegate/parsers/fortran_fparser.py +1116 -0
  39. nativegate/parsers/fortran_regex.py +686 -0
  40. nativegate/preprocess.py +335 -0
  41. nativegate/structural_invariants.py +762 -0
  42. nativegate/suggest.py +208 -0
  43. nativegate/templates/__init__.py +20 -0
  44. nativegate/templates/golden_test_template.py +248 -0
  45. nativegate/wire.py +438 -0
  46. nativegate-0.1.0.dist-info/METADATA +547 -0
  47. nativegate-0.1.0.dist-info/RECORD +50 -0
  48. nativegate-0.1.0.dist-info/WHEEL +5 -0
  49. nativegate-0.1.0.dist-info/entry_points.txt +3 -0
  50. nativegate-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,406 @@
1
+ """pybind11 binding generator (design.md section 7)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from ..ir import ClassDef, FunctionDef, Method, ModuleIR, Parameter, StructDef
8
+
9
+ # Python type name -> the C++ spelling pybind11 needs in an init<> signature.
10
+ # Only the scalar types need translating; record types are already spelled
11
+ # with their C++ name in the IR.
12
+ _CPP_SPELLING = {
13
+ "int": "int",
14
+ "float": "double",
15
+ "bool": "bool",
16
+ "str": "std::string",
17
+ "None": "void",
18
+ }
19
+
20
+
21
+ def _qualified(name: str, namespace: str | None) -> str:
22
+ return f"{namespace}::{name}" if namespace else name
23
+
24
+
25
+ # Type spellings that are already complete on their own: qualifying them with
26
+ # the enclosing namespace would invent `petro::double`.
27
+ _BUILTIN_WORDS = {
28
+ "void", "bool", "char", "wchar_t", "char8_t", "char16_t", "char32_t",
29
+ "short", "int", "long", "float", "double", "signed", "unsigned",
30
+ "const", "volatile", "auto",
31
+ }
32
+ _STDINT_RE = re.compile(r"^(u?int(_least|_fast)?(8|16|32|64)_t|u?intptr_t|u?intmax_t|size_t|ssize_t|ptrdiff_t)$")
33
+
34
+
35
+ def _needs_qualification(spelling: str) -> bool:
36
+ """True when `spelling` names a user type written without its namespace."""
37
+ core = spelling.replace("const", " ").replace("volatile", " ")
38
+ core = core.replace("&", " ").replace("*", " ").strip()
39
+ if not core or "::" in core or "<" in core:
40
+ return False
41
+ tokens = core.split()
42
+ if len(tokens) != 1:
43
+ # `unsigned long`, `long double`, ... — all builtin words.
44
+ return False
45
+ token = tokens[0]
46
+ return token not in _BUILTIN_WORDS and not _STDINT_RE.match(token)
47
+
48
+
49
+ def _qualify_spelling(spelling: str, namespace: str | None) -> str:
50
+ if namespace and _needs_qualification(spelling):
51
+ # Keep any cv/ref decoration in place, qualify only the type name.
52
+ return re.sub(r"([A-Za-z_]\w*)", lambda m: f"{namespace}::{m.group(1)}"
53
+ if m.group(1) not in _BUILTIN_WORDS else m.group(1), spelling, count=0)
54
+ return spelling
55
+
56
+
57
+ def _param_spelling(param: Parameter, namespace: str | None) -> str:
58
+ """The C++ type to emit for one parameter.
59
+
60
+ Prefer the native spelling the parser recorded; `Parameter.type` is the
61
+ *Python* name and is lossy by construction (uint64_t, int and an unscoped
62
+ enum all collapse to "int"), so `_CPP_SPELLING` is only the fallback for
63
+ IR produced before native types were recorded.
64
+ """
65
+ if param.native_type is not None:
66
+ # Emitted verbatim. The parser resolves each type through its own
67
+ # declaration and qualifies it there (`petro::Correlation`,
68
+ # `std::uint64_t`), so it already knows the answer — whereas this
69
+ # generator only knows the namespace of the record being bound.
70
+ # Re-qualifying here guessed that every unqualified name lived in that
71
+ # same namespace, so a `ns::Widget` method taking a global-namespace
72
+ # `Color` emitted `ns::Color` and failed to compile with "no type
73
+ # named 'Color' in namespace 'ns'". A name the parser left unqualified
74
+ # is one that resolves unqualified at namespace scope, which is
75
+ # exactly where the generated binding writes it.
76
+ return param.native_type
77
+ return _CPP_SPELLING.get(param.type, _qualified(param.type, namespace))
78
+
79
+
80
+ def _init_signature(params: list[Parameter], namespace: str | None) -> str:
81
+ """C++ type list for `py::init<...>`."""
82
+ return ", ".join(_param_spelling(p, namespace) for p in params)
83
+
84
+
85
+ def _overloaded_target(
86
+ target: str,
87
+ params: list[Parameter],
88
+ namespace: str | None,
89
+ is_const: bool = False,
90
+ ) -> str:
91
+ """`&f` is ambiguous once `f` is overloaded; spell the signature out."""
92
+ args = ", ".join(_param_spelling(p, namespace) for p in params)
93
+ extra = ", py::const_" if is_const else ""
94
+ return f"py::overload_cast<{args}>(&{target}{extra})"
95
+
96
+
97
+ def _method_target(method: Method, qualified: str, namespace: str | None) -> str:
98
+ if _buffer_parameters(method) or _scalar_ref_parameters(method):
99
+ return _buffer_lambda(method, self_type=qualified)
100
+ # `cpp_name` when the published name is not the C++ one — an operator is
101
+ # bound as a Python special method, so `&Cls::__add__` would name nothing.
102
+ target = f"{qualified}::{getattr(method, 'cpp_name', None) or method.name}"
103
+ if getattr(method, "is_overloaded", False):
104
+ return _overloaded_target(
105
+ target, method.parameters, namespace, getattr(method, "is_const", False)
106
+ )
107
+ return f"&{target}"
108
+
109
+
110
+ def _emit_record(lines: list[str], record: ClassDef | StructDef) -> None:
111
+ # Each record carries its own namespace: a service merging headers from two
112
+ # namespaces used to have the first record's namespace applied to all of
113
+ # them, and a struct-only service got None and emitted unqualified names.
114
+ namespace = record.namespace
115
+ qualified = _qualified(record.name, namespace)
116
+ # `py::class_<Derived, Base>` is what teaches pybind11 the relationship:
117
+ # without it, inherited methods are missing from Python and a Base& return
118
+ # never upcasts to the derived wrapper.
119
+ bases = getattr(record, "bases", [])
120
+ template_args = ", ".join([qualified, *(_qualified(b, namespace) for b in bases)])
121
+ lines.append(f' py::class_<{template_args}>(m, "{record.name}")')
122
+
123
+ body: list[str] = []
124
+
125
+ # A struct with a const or reference member has no default constructor;
126
+ # emitting py::init<>() for one is a compile error.
127
+ if record.has_default_constructor:
128
+ body.append(" .def(py::init<>())")
129
+
130
+ if not isinstance(record, StructDef):
131
+ for ctor in record.constructors:
132
+ body.append(f" .def(py::init<{_init_signature(ctor, namespace)}>())")
133
+ for method in record.methods:
134
+ target = _method_target(method, qualified, namespace)
135
+ body.append(f' .def("{method.name}", {target})')
136
+
137
+ for field in record.fields:
138
+ # A const member cannot be bound read/write: pybind11's def_readwrite
139
+ # assigns through the pointer-to-member and fails to compile.
140
+ binder = "def_readonly" if getattr(field, "is_const", False) else "def_readwrite"
141
+ body.append(
142
+ f' .{binder}("{field.name}", &{qualified}::{field.name})'
143
+ )
144
+
145
+ if not body:
146
+ # A record with nothing bindable still needs a terminated statement.
147
+ lines[-1] += ";"
148
+ return
149
+
150
+ for i, line in enumerate(body):
151
+ lines.append(line + (";" if i == len(body) - 1 else ""))
152
+
153
+
154
+ def _buffer_parameters(func: FunctionDef) -> list[Parameter]:
155
+ """Parameters bound from a bare `T*` whose length another argument carries."""
156
+ return [p for p in func.parameters if getattr(p, "length_param", None)]
157
+
158
+
159
+ def _scalar_ref_parameters(func: FunctionDef) -> list[Parameter]:
160
+ """Scalars passed by reference — extern "C" Fortran-linkage arguments."""
161
+ return [p for p in func.parameters if getattr(p, "is_scalar_ref", False)]
162
+
163
+
164
+ # numpy dtype for each element type nativegate binds through a raw pointer.
165
+ _NUMPY_DTYPE = {"float": "double", "int": "int", "bool": "bool"}
166
+
167
+
168
+ def _buffer_lambda(func, self_type: str | None = None) -> str:
169
+ """A lambda binding `f(T* data, int n, ...)` as `f(data, ...)`.
170
+
171
+ With `self_type`, the same lambda binds a METHOD: pybind11 passes the
172
+ instance as the first argument of a def'd callable, so
173
+ `[](Simulator& self, py::array values) { return self.set_porosity(...); }`
174
+ is a complete method binding — `int set_porosity(double* values, int n)`
175
+ is the single most common shape a numerical C++ class exposes, and it used
176
+ to be refused with "not yet for class methods".
177
+
178
+ THE LENGTH DISAPPEARS FROM THE PYTHON SIGNATURE, on purpose. It is read off
179
+ the buffer the caller actually passed, so `n` cannot disagree with the data
180
+ — which is the entire failure mode of a C array API. The Fortran side has
181
+ to keep its `n` (f2py fixes the signature) and validates it instead; here
182
+ the generator controls the signature, so the mismatch is made unspellable.
183
+
184
+ Mutable and read-only buffers are bound differently, and the difference is
185
+ load-bearing rather than stylistic:
186
+
187
+ * A **read-only** `const T*` is input. pybind11 may freely convert a list
188
+ or a wrong-dtype array into a temporary, because nothing is written back.
189
+ `forcecast` makes the endpoint convenient.
190
+
191
+ * A **mutable** `T*` is one the callee writes through. Conversion there is
192
+ catastrophic and silent: pybind11 would write into a temporary and
193
+ discard it. Measured, before this was written — `py::array_t<double>`
194
+ handed an int64 array accepted it, converted, and lost every write; and
195
+ `request()` without `true` wrote straight through a `writeable=False`
196
+ array. So a mutable buffer takes an untyped `py::array`, checks the dtype
197
+ itself, and calls `request(true)`, which raises on a read-only array.
198
+ """
199
+ namespace = getattr(func, "namespace", None)
200
+ if self_type is not None:
201
+ target = f"self.{getattr(func, 'cpp_name', None) or func.name}"
202
+ else:
203
+ target = _qualified(func.name, namespace)
204
+ buffers = {p.name for p in _buffer_parameters(func)}
205
+ lengths = {p.length_param for p in _buffer_parameters(func)}
206
+
207
+ sig: list[str] = []
208
+ body: list[str] = []
209
+ call_args: list[str] = []
210
+ arg_names: list[str] = []
211
+
212
+ for param in func.parameters:
213
+ if param.name in lengths:
214
+ # Supplied from the buffer's own size; never asked of the caller.
215
+ continue
216
+ if param.name in buffers:
217
+ dtype = _NUMPY_DTYPE[param.type]
218
+ arg_names.append(param.name)
219
+ if param.is_mutable_buffer:
220
+ sig.append(f"py::array {param.name}")
221
+ body.append(
222
+ f' if (!{param.name}.dtype().is(py::dtype::of<{dtype}>()))\n'
223
+ f' throw py::type_error("{func.name}(): \'{param.name}\' must be a '
224
+ f'numpy array of dtype {dtype}; nativegate will not convert it because '
225
+ f'this function writes through the buffer and a converted array is a '
226
+ f'temporary whose writes would be discarded.");'
227
+ )
228
+ body.append(
229
+ f' if ({param.name}.ndim() != 1)\n'
230
+ f' throw py::value_error("{func.name}(): \'{param.name}\' must be 1-D.");'
231
+ )
232
+ # request(true) raises on a read-only array. Without it,
233
+ # pybind11 writes straight through `writeable=False`.
234
+ body.append(f" auto {param.name}_info = {param.name}.request(true);")
235
+ else:
236
+ sig.append(
237
+ f"py::array_t<{dtype}, py::array::c_style | py::array::forcecast> "
238
+ f"{param.name}"
239
+ )
240
+ body.append(f" auto {param.name}_info = {param.name}.request();")
241
+ continue
242
+ if getattr(param, "is_scalar_ref", False):
243
+ sig.append(f"{_CPP_SPELLING[param.type]} {param.name}")
244
+ else:
245
+ sig.append(f"{_param_spelling(param, namespace)} {param.name}")
246
+ arg_names.append(param.name)
247
+
248
+ for param in func.parameters:
249
+ if getattr(param, "is_scalar_ref", False):
250
+ # The value came in by value; the callee wants its address.
251
+ call_args.append(f"&{param.name}")
252
+ elif param.name in buffers:
253
+ const = "" if param.is_mutable_buffer else "const "
254
+ dtype = _NUMPY_DTYPE[param.type]
255
+ call_args.append(f"static_cast<{const}{dtype}*>({param.name}_info.ptr)")
256
+ elif param.name in lengths:
257
+ owner = next(p for p in _buffer_parameters(func) if p.length_param == param.name)
258
+ call_args.append(
259
+ f"static_cast<{param.cpp_spelling('int')}>({owner.name}_info.size)"
260
+ )
261
+ else:
262
+ call_args.append(param.name)
263
+
264
+ invocation = f"{target}({', '.join(call_args)})"
265
+ # Non-const scalar refs are OUTPUTS: the callee wrote through the address
266
+ # of a lambda local, and that local dies with the call. Returning the
267
+ # final values is what stops PVTERR's `int* icode` vanishing silently.
268
+ # The shape follows f2py's convention, which the Fortran router already
269
+ # speaks: result first, then each output in declaration order; a bare
270
+ # value when there is exactly one thing to return.
271
+ outputs = [
272
+ p.name
273
+ for p in func.parameters
274
+ if getattr(p, "is_scalar_ref", False) and p.intent != "in"
275
+ ]
276
+ if not outputs:
277
+ body.append(
278
+ f" {invocation};"
279
+ if func.returns == "None"
280
+ else f" return {invocation};"
281
+ )
282
+ elif func.returns == "None":
283
+ body.append(f" {invocation};")
284
+ if len(outputs) == 1:
285
+ body.append(f" return {outputs[0]};")
286
+ else:
287
+ body.append(
288
+ f" return py::make_tuple({', '.join(outputs)});"
289
+ )
290
+ else:
291
+ body.append(f" auto result = {invocation};")
292
+ body.append(
293
+ f" return py::make_tuple(result, {', '.join(outputs)});"
294
+ )
295
+
296
+ py_args = "".join(f', py::arg("{n}")' for n in arg_names)
297
+ if self_type is not None:
298
+ # `self` is positional-only from pybind11's point of view; naming it
299
+ # with py::arg would shift every argument name by one.
300
+ sig = [f"{self_type}& self"] + sig
301
+ lines = [f"[]({', '.join(sig)}) {{"]
302
+ lines += body
303
+ lines.append(" }")
304
+ return "\n".join(lines) + py_args
305
+
306
+
307
+ def _function_target(func: FunctionDef) -> str:
308
+ """`&f` for a free function, qualified by its own namespace.
309
+
310
+ Free functions used to be emitted unqualified, so anything outside the
311
+ global namespace failed with "use of undeclared identifier".
312
+ """
313
+ if _buffer_parameters(func) or _scalar_ref_parameters(func):
314
+ return _buffer_lambda(func)
315
+ namespace = getattr(func, "namespace", None)
316
+ target = _qualified(func.name, namespace)
317
+ if getattr(func, "is_overloaded", False):
318
+ return _overloaded_target(target, func.parameters, namespace)
319
+ return f"&{target}"
320
+
321
+
322
+ def _base_first(classes: list[ClassDef]) -> list[ClassDef]:
323
+ """Order classes so every base is registered before anything derived.
324
+
325
+ `py::class_<Derived, Base>` requires Base to already exist in the module;
326
+ registering them in header order raises "generic_type: type Base is not
327
+ registered" at *import* time, long after the build succeeded. Headers are
328
+ merged alphabetically, so this is not a rare ordering — it is a coin flip.
329
+
330
+ A cycle is impossible in valid C++ (you cannot derive from an incomplete
331
+ type), but the walk is guarded anyway so a malformed IR cannot hang codegen.
332
+ """
333
+ by_name = {cls.name: cls for cls in classes}
334
+ ordered: list[ClassDef] = []
335
+ placed: set[str] = set()
336
+
337
+ def place(cls: ClassDef, pending: frozenset[str]) -> None:
338
+ if cls.name in placed or cls.name in pending:
339
+ return
340
+ for base in cls.bases:
341
+ parent = by_name.get(base)
342
+ if parent is not None:
343
+ place(parent, pending | {cls.name})
344
+ placed.add(cls.name)
345
+ ordered.append(cls)
346
+
347
+ for cls in classes:
348
+ place(cls, frozenset())
349
+ return ordered
350
+
351
+
352
+ def generate_bindings(module: ModuleIR, header_names: str | list[str]) -> str:
353
+ """Emit one pybind11 module binding every exposed symbol.
354
+
355
+ `header_names` may be several headers: a service with foo.hpp and bar.hpp
356
+ produces ONE extension including both. A Python extension can only have a
357
+ single PYBIND11_MODULE (one init symbol per .so), so generating one
358
+ bindings file per header would leave all but one orphaned — which is
359
+ exactly what used to happen, silently dropping those classes.
360
+ """
361
+ if isinstance(header_names, str):
362
+ header_names = [header_names]
363
+
364
+ module_symbol = f"{module.name}_cpp"
365
+
366
+ # Only when something is actually bound through a raw pointer: numpy.h
367
+ # pulls in numpy's headers at build time and numpy at runtime, and a C++
368
+ # service that binds no buffers should not acquire that dependency.
369
+ needs_numpy = any(_buffer_parameters(fn) for fn in module.functions) or any(
370
+ _buffer_parameters(m) for cls in module.classes for m in cls.methods
371
+ )
372
+
373
+ lines = [
374
+ "// Generated by nativegate. Do not edit by hand — re-run `ngate generate`.",
375
+ "#include <pybind11/pybind11.h>",
376
+ # std::vector<T> <-> Python list. The conversion is a COPY in both
377
+ # directions, which is why the parser refuses a non-const
378
+ # `std::vector<T>&`: writes through one land in the temporary and are
379
+ # discarded. See parsers/cpp_ast._refuse_mutable_vector_ref.
380
+ "#include <pybind11/stl.h>",
381
+ *(["#include <pybind11/numpy.h>"] if needs_numpy else []),
382
+ "#include <cstdint>",
383
+ "#include <string>",
384
+ "#include <vector>",
385
+ "",
386
+ *[f'#include "{h}"' for h in header_names],
387
+ "",
388
+ "namespace py = pybind11;",
389
+ "",
390
+ f"PYBIND11_MODULE({module_symbol}, m) {{",
391
+ ]
392
+
393
+ # Structs first: a method returning one can only be bound after the type
394
+ # itself is registered with pybind11.
395
+ for struct in module.structs:
396
+ _emit_record(lines, struct)
397
+
398
+ for cls in _base_first(module.classes):
399
+ _emit_record(lines, cls)
400
+
401
+ for func in module.functions:
402
+ lines.append(f' m.def("{func.name}", {_function_target(func)});')
403
+
404
+ lines.append("}")
405
+ lines.append("")
406
+ return "\n".join(lines)
@@ -0,0 +1,61 @@
1
+ """Generated pyproject.toml for a service (design.md sections 10, 14)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _BUILD_REQUIRES = {
6
+ "cpp": '["scikit-build-core", "pybind11"]',
7
+ "fortran": '["scikit-build-core", "numpy", "meson", "ninja"]',
8
+ }
9
+
10
+ # gunicorn supervises the worker processes; see docker_gen's CMD and
11
+ # docs/deployment-topologies.md. It is a RUNTIME dependency, not a dev one:
12
+ # a bare `uvicorn` is a single process, so one segfault in native code takes
13
+ # the whole service down with nothing left to restart it.
14
+ #
15
+ # `uvicorn-worker` rather than `uvicorn.workers.UvicornWorker`: the in-uvicorn
16
+ # worker class is deprecated and slated for removal, and shipping a deprecated
17
+ # import path inside generated code nobody is allowed to hand-edit is how a
18
+ # service breaks on a routine dependency bump.
19
+ #
20
+ # fastmcp is a RUNTIME dependency, not an optional extra: `service.py` imports
21
+ # mcp_server unconditionally, so a wheel without it does not start at all. The
22
+ # MCP endpoint is part of every generated service's surface by design — an
23
+ # LLM-callable interface that has to be switched on is one that is not there
24
+ # when someone goes looking for it. See generators/mcp_gen.py.
25
+ _DEPENDENCIES = {
26
+ "cpp": '["fastapi", "uvicorn", "gunicorn", "uvicorn-worker", "fastmcp"]',
27
+ "fortran": '["fastapi", "uvicorn", "gunicorn", "uvicorn-worker", "fastmcp", "numpy"]',
28
+ }
29
+
30
+
31
+ def generate_pyproject(
32
+ service_name: str, language: str, needs_numpy: bool = False
33
+ ) -> str:
34
+ """The generated service's pyproject.toml.
35
+
36
+ `needs_numpy` is set for a C++ service that binds a raw `T*` as a numpy
37
+ buffer: pybind11/numpy.h needs numpy's headers to build and numpy to
38
+ import. A C++ service with no such pointer does not get the dependency —
39
+ declaring it unconditionally would put numpy in every image for the
40
+ benefit of the services that do not use it.
41
+ """
42
+ build_requires = _BUILD_REQUIRES.get(language, _BUILD_REQUIRES["cpp"])
43
+ dependencies = _DEPENDENCIES.get(language, _DEPENDENCIES["cpp"])
44
+ if needs_numpy and '"numpy"' not in dependencies:
45
+ build_requires = build_requires.rstrip("]") + ', "numpy"]'
46
+ dependencies = dependencies.rstrip("]") + ', "numpy"]'
47
+
48
+ return f"""[build-system]
49
+ requires = {build_requires}
50
+ build-backend = "scikit_build_core.build"
51
+
52
+ [project]
53
+ name = "{service_name}"
54
+ version = "1.0.0"
55
+ requires-python = ">=3.10"
56
+ dependencies = {dependencies}
57
+
58
+ [tool.scikit-build]
59
+ wheel.packages = ["python/{service_name}"]
60
+ cmake.source-dir = "."
61
+ """