numbox 0.2.5__py3-none-any.whl → 0.2.7__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 numbox might be problematic. Click here for more details.

numbox/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.2.5'
1
+ __version__ = '0.2.7'
@@ -7,20 +7,13 @@ from numba.extending import intrinsic # noqa: F401
7
7
  from types import FunctionType as PyFunctionType
8
8
  from typing import List, Optional, Tuple
9
9
 
10
+ from numbox.utils.standard import make_params_strings
11
+
10
12
 
11
13
  def make_proxy_name(name):
12
14
  return f'__{name}'
13
15
 
14
16
 
15
- def make_params_strings(func):
16
- func_params = inspect.signature(func).parameters
17
- func_params_str = ', '.join(
18
- [k if v.default == inspect._empty else f'{k}={v.default}' for k, v in func_params.items()]
19
- )
20
- func_names_params_str = ', '.join(func_params.keys())
21
- return func_params_str, func_names_params_str
22
-
23
-
24
17
  def proxy(sig, jit_options: Optional[dict] = None):
25
18
  """ Create a proxy for the decorated function `func` with the given signature(s) `sig`.
26
19
 
@@ -1,10 +1,11 @@
1
- from hashlib import md5
1
+ from collections import namedtuple
2
+ from hashlib import sha256
2
3
  from inspect import getfile, getmodule, getsource
3
4
  from io import StringIO
4
5
  from itertools import chain
5
6
  from numba import njit, typeof
6
7
  from numba.core.types import Type
7
- from typing import Any, Callable, NamedTuple, Optional, Sequence, Union
8
+ from typing import Any, Callable, Dict, NamedTuple, Optional, Sequence, Union
8
9
 
9
10
  from numbox.core.configurations import default_jit_options
10
11
  from numbox.core.work.lowlevel_work_utils import ll_make_work
@@ -15,7 +16,7 @@ def _file_anchor():
15
16
  raise NotImplementedError
16
17
 
17
18
 
18
- _nodes_names = set()
19
+ _specs_registry = dict()
19
20
 
20
21
 
21
22
  class _End(NamedTuple):
@@ -27,10 +28,11 @@ class _End(NamedTuple):
27
28
  def _new(cls, super_proxy, *args, **kwargs):
28
29
  name = kwargs.get("name")
29
30
  assert name, "`name` key-word argument has not been provided"
30
- if name in _nodes_names:
31
+ if name in _specs_registry:
31
32
  raise ValueError(f"Node '{name}' has already been defined on this graph. Pick a different name.")
32
- _nodes_names.add(name)
33
- return super_proxy.__new__(cls, *args, **kwargs)
33
+ spec_ = super_proxy.__new__(cls, *args, **kwargs)
34
+ _specs_registry[name] = spec_
35
+ return spec_
34
36
 
35
37
 
36
38
  class End(_End):
@@ -100,7 +102,7 @@ def _derived_line(
100
102
  sources_ = sources_ + ", " if "," not in sources_ else sources_
101
103
  ty_ = get_ty(derived_)
102
104
  derive_func = derived_.derive
103
- derive_hashes.append(md5(getsource(derive_func).encode("utf-8")).hexdigest())
105
+ derive_hashes.append(sha256(getsource(derive_func).encode("utf-8")).hexdigest())
104
106
  derive_ = _derived_cres(ty_, derived_.sources, derive_func, jit_options)
105
107
  derive_name = f"{name_}_derive"
106
108
  init_name = f"{name_}_init"
@@ -111,29 +113,34 @@ def _derived_line(
111
113
  return f"""{name_} = ll_make_work("{name_}", {init_name}, ({sources_}), {derive_name})"""
112
114
 
113
115
 
114
- def _verify_access_nodes(
115
- all_inputs_: Sequence[End],
116
- all_derived_: Sequence[Derived],
117
- access_nodes: Sequence[SpecTy]
118
- ):
119
- for access_node in access_nodes:
120
- assert access_node in all_inputs_ or access_node in all_derived_, f"{access_node} cannot be reached"
121
-
122
-
123
116
  def code_block_hash(code_txt: str):
124
117
  """ Re-compile and re-save cache when source code has changed. """
125
- return md5(code_txt.encode("utf-8")).hexdigest()
118
+ return sha256(code_txt.encode("utf-8")).hexdigest()
126
119
 
127
120
 
128
- def make_graph(
129
- all_inputs_: Sequence[End],
130
- all_derived_: Sequence[Derived],
131
- access_nodes: SpecTy | Sequence[SpecTy],
132
- jit_options: Optional[dict] = None
133
- ):
134
- if isinstance(access_nodes, SpecTy):
135
- access_nodes = (access_nodes,)
136
- _verify_access_nodes(all_inputs_, all_derived_, access_nodes)
121
+ def _infer_end_and_derived_nodes(spec: SpecTy, all_inputs_: Dict[str, Type], all_derived_: Dict[str, Type]):
122
+ if spec.name in all_inputs_ or spec.name in all_derived_:
123
+ return
124
+ if isinstance(spec, End):
125
+ all_inputs_[spec.name] = get_ty(spec)
126
+ return
127
+ for source in spec.sources:
128
+ _infer_end_and_derived_nodes(source, all_inputs_, all_derived_)
129
+ all_derived_[spec.name] = get_ty(spec)
130
+
131
+
132
+ def infer_end_and_derived_nodes(access_nodes: SpecTy | Sequence[SpecTy]):
133
+ all_inputs_ = dict()
134
+ all_derived_ = dict()
135
+ for access_node in access_nodes:
136
+ _infer_end_and_derived_nodes(access_node, all_inputs_, all_derived_)
137
+ all_inputs_lst = [_specs_registry[name] for name in all_inputs_.keys()]
138
+ all_derived_lst = [_specs_registry[name] for name in all_derived_.keys()]
139
+ return all_inputs_lst, all_derived_lst
140
+
141
+
142
+ def make_graph(*access_nodes: SpecTy | Sequence[SpecTy], jit_options: Optional[dict] = None):
143
+ all_inputs_, all_derived_ = infer_end_and_derived_nodes(access_nodes)
137
144
  if jit_options is None:
138
145
  jit_options = {}
139
146
  jit_options = {**default_jit_options, **jit_options}
@@ -144,26 +151,30 @@ def make_graph(
144
151
  _make_args = []
145
152
  code_txt = StringIO()
146
153
  initializers = {}
147
- derive_hashes=[]
154
+ derive_hashes = []
148
155
  for input_ in all_inputs_:
149
156
  line_ = _input_line(input_, ns, initializers)
150
157
  code_txt.write(f"\n\t{line_}")
151
158
  for derived_ in all_derived_:
152
159
  line_ = _derived_line(derived_, ns, initializers, derive_hashes, _make_args, jit_options)
153
160
  code_txt.write(f"\n\t{line_}")
154
- hash_str = f"code_block = {code_txt.getvalue()} initializers = {list(initializers.values())} derive_hashes = {derive_hashes}"
161
+ hash_str = f"code_block = {code_txt.getvalue()} initializers = {list(initializers.values())} derive_hashes = {derive_hashes}" # noqa: E501
155
162
  hash_ = code_block_hash(hash_str)
156
- code_txt.write(f"""\n\taccess_tuple = ({", ".join([n.name for n in access_nodes])})""")
157
- code_txt.write(f"\n\treturn access_tuple")
163
+ access_nodes_names = [n.name for n in access_nodes]
164
+ tup_ = ", ".join(access_nodes_names)
165
+ tup_ = tup_ + ", " if ", " not in tup_ else tup_
166
+ code_txt.write(f"""\n\taccess_tuple = ({tup_})""")
167
+ code_txt.write("\n\treturn access_tuple")
158
168
  code_txt = code_txt.getvalue()
159
169
  make_params = ", ".join(chain(_make_args, initializers.keys()))
160
170
  make_name = f"_make_{hash_}"
161
171
  code_txt = f"""
162
172
  @njit(**jit_options)
163
173
  def {make_name}({make_params}):""" + code_txt + f"""
164
- return_node_ = {make_name}({make_params})
174
+ access_tuple_ = {make_name}({make_params})
165
175
  """
166
176
  code = compile(code_txt, getfile(_file_anchor), mode="exec")
167
177
  exec(code, ns)
168
- return_node_ = ns["return_node_"]
169
- return return_node_
178
+ access_tuple_ = ns["access_tuple_"]
179
+ Access = namedtuple("Access", access_nodes_names)
180
+ return Access(*access_tuple_)
@@ -30,6 +30,9 @@ def _explain(work: Work, derivation_: list, derived_: set):
30
30
 
31
31
 
32
32
  def explain(work: Work):
33
+ all_end_nodes = work.all_end_nodes()
33
34
  derivation = []
34
35
  _explain(work, derivation, set())
35
- return "\n".join(derivation)
36
+ explain_txt = f"All required end nodes: {all_end_nodes}\n\n"
37
+ explain_txt += "\n".join(derivation)
38
+ return explain_txt
numbox/utils/highlevel.py CHANGED
@@ -1,4 +1,7 @@
1
1
  import hashlib
2
+ import re
3
+ from inspect import getfile, getmodule, getsource
4
+ from io import StringIO
2
5
  from numba import njit
3
6
  from numba.core.itanium_mangler import mangle_type_or_value
4
7
  from numba.core.types import Type
@@ -6,6 +9,17 @@ from numba.core.types.functions import Dispatcher
6
9
  from numba.core.types.function_type import CompileResultWAP
7
10
  from numba.core.typing.templates import Signature
8
11
  from numba.experimental.function_type import FunctionType
12
+ from numba.experimental.structref import define_boxing, new, StructRefProxy
13
+ from numba.extending import overload, overload_method
14
+ from textwrap import dedent, indent
15
+ from typing import Callable, Dict, Iterable, Optional
16
+
17
+ from numbox.core.configurations import default_jit_options
18
+ from numbox.utils.standard import make_params_strings
19
+
20
+
21
+ def _file_anchor():
22
+ raise NotImplementedError
9
23
 
10
24
 
11
25
  def cres(sig, **kwargs):
@@ -32,7 +46,164 @@ def determine_field_index(struct_ty, field_name):
32
46
 
33
47
  def hash_type(ty: Type):
34
48
  mangled_ty = mangle_type_or_value(ty)
35
- return hashlib.sha256(mangled_ty.encode('utf-8')).hexdigest()
49
+ return hashlib.sha256(mangled_ty.encode("utf-8")).hexdigest()
50
+
51
+
52
+ def make_structref_code_txt(
53
+ struct_name: str,
54
+ struct_fields: Iterable[str] | Dict[str, Type],
55
+ struct_type_class: type | Type,
56
+ struct_methods: Optional[Dict[str, Callable]] = None,
57
+ jit_options: Optional[dict] = None
58
+ ):
59
+ if isinstance(struct_fields, dict):
60
+ struct_fields, fields_types = list(struct_fields.keys()), list(struct_fields.values())
61
+ else:
62
+ assert isinstance(struct_fields, (list, tuple)), struct_fields
63
+ fields_types = None
64
+ struct_fields_str = ", ".join([field for field in struct_fields])
65
+ make_name = f"make_{struct_name.lower()}"
66
+ new_returns = f"{make_name}({struct_fields_str})"
67
+ repr_str = f"f'{struct_name}(" + ", ".join([f"{field}={{self.{field}}}" for field in struct_fields]) + ")'"
68
+ code_txt = StringIO()
69
+ code_txt.write(f"""
70
+ class {struct_name}(StructRefProxy):
71
+ def __new__(cls, {struct_fields_str}):
72
+ return {new_returns}
73
+
74
+ def __repr__(self):
75
+ return {repr_str}
76
+ """)
77
+ for field in struct_fields:
78
+ code_txt.write(f"""
79
+ @property
80
+ @njit(**jit_options)
81
+ def {field}(self):
82
+ return self.{field}
83
+ """)
84
+ methods_code_txt = StringIO()
85
+ if struct_methods is not None:
86
+ assert isinstance(struct_methods, dict), f"""
87
+ Expected dictionary of methods names to callable, got {struct_methods}"""
88
+ for method_name, method in struct_methods.items():
89
+ params_str, names_params_str = make_params_strings(method)
90
+ names_params_lst = names_params_str.split(", ")
91
+ self_name = names_params_lst[0]
92
+ names_params_str_wo_self = ", ".join(names_params_lst[1:])
93
+ method_source = dedent(getsource(method))
94
+ method_hash = hashlib.sha256(method_source.encode("utf-8")).hexdigest()
95
+ code_txt.write(f"""
96
+ def {method_name}({params_str}):
97
+ return {self_name}.{method_name}_{method_hash}({names_params_str_wo_self})
98
+
99
+ @njit(**jit_options)
100
+ def {method_name}_{method_hash}({params_str}):
101
+ return {self_name}.{method_name}({names_params_str_wo_self})
102
+ """)
103
+ method_header = re.findall(r"^\s*def\s+([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*:[^\n]*", method_source, re.MULTILINE)
104
+ assert len(method_header) == 1, method_header
105
+ method_name, params_str_ = method_header[0]
106
+ assert params_str == params_str_, (params_str, params_str_)
107
+ method_source = re.sub(r"\bdef\s+([a-zA-Z_]\w*)\b", f"def _", method_source)
108
+ methods_code_txt.write(f"""
109
+ @overload_method({struct_type_class.__name__}, "{method_name}", jit_options=jit_options)
110
+ def ol_{method_name}({params_str}):
111
+ {indent(method_source, " ")}
112
+ return _
113
+ """)
114
+ code_txt.write(f"""
115
+ define_boxing({struct_type_class.__name__}, {struct_name})
116
+ """)
117
+ struct_type_name = f"{struct_name}Type"
118
+ struct_fields_ty_str = ", ".join([f"{field}_ty" for field in struct_fields])
119
+ struct_type_code_block = ""
120
+ if fields_types is None:
121
+ struct_type_code_block = f"""fields_types = [{struct_fields_ty_str}]
122
+ fields_and_their_types = list(zip(fields, fields_types))
123
+ {struct_name}Type = {struct_type_class.__name__}(fields_and_their_types)
124
+ """
125
+ else:
126
+ code_txt.write(f"""
127
+ fields_and_their_types = list(zip(fields, fields_types))
128
+ {struct_name}Type = {struct_type_class.__name__}(fields_and_their_types)
129
+ """)
130
+ ctor_code_block = "\n".join([f" struct_.{field} = {field}" for field in struct_fields])
131
+ code_txt.write(f"""
132
+ @overload({struct_name}, strict=False, jit_options=jit_options)
133
+ def ol_{struct_name.lower()}({struct_fields_ty_str}):
134
+ {struct_type_code_block}
135
+ def ctor({struct_fields_str}):
136
+ struct_ = new({struct_type_name})
137
+ {ctor_code_block}
138
+ return struct_
139
+ return ctor
140
+ """)
141
+ if fields_types is not None:
142
+ code_txt.write(f"""
143
+ {make_name}_sig = {struct_name}Type(*fields_types)
144
+ """)
145
+ else:
146
+ code_txt.write(f"""
147
+ {make_name}_sig = None
148
+ """)
149
+ code_txt.write(f"""
150
+ @njit({make_name}_sig, **jit_options)
151
+ def {make_name}({struct_fields_str}):
152
+ return {struct_name}({struct_fields_str})
153
+ """)
154
+ code_txt = code_txt.getvalue() + methods_code_txt.getvalue()
155
+ return code_txt, fields_types
156
+
157
+
158
+ def make_structref(
159
+ struct_name: str,
160
+ struct_fields: Iterable[str] | Dict[str, Type],
161
+ struct_type_class: type | Type,
162
+ *,
163
+ struct_methods: Optional[Dict[str, Callable]] = None,
164
+ jit_options: Optional[dict] = None
165
+ ):
166
+ """
167
+ Makes structure type with `struct_name` and `struct_fields` from the StructRef type class.
168
+
169
+ A unique `struct_type_class` for each structref needs to be provided.
170
+ If caching of code that will be using the created struct type is desired,
171
+ these type class(es) need/s to be defined in a python module that is *not* executed.
172
+ (Same requirement is also to observed even when the full definition of StructRef
173
+ is entirely hard-coded rather than created dynamically.)
174
+
175
+ In particular, that's why `struct_type_class` cannot be incorporated into
176
+ the dynamic compile / exec routine here.
177
+
178
+ Dictionary of methods to be bound to the created structref can be provided as well.
179
+ Struct methods will get inlined into the caller if numba deems it to be optimal
180
+ (even if `jit_options` says otherwise), therefore changing the methods code
181
+ without poking the jitted caller can result in a stale cache - when the latter is
182
+ cached. This is not an exclusive limitation of a dynamic structref creation via
183
+ this function and is equally true when the structref definition is coded explicitly.
184
+ """
185
+ code_txt, fields_types = make_structref_code_txt(
186
+ struct_name, struct_fields, struct_type_class, struct_methods, jit_options
187
+ )
188
+ if jit_options is None:
189
+ jit_options = default_jit_options
190
+ ns = {
191
+ **getmodule(_file_anchor).__dict__,
192
+ **{
193
+ "fields": struct_fields,
194
+ "fields_types": fields_types,
195
+ "define_boxing": define_boxing,
196
+ "jit_options": jit_options,
197
+ "new": new,
198
+ "njit": njit,
199
+ "overload_method": overload_method,
200
+ "StructRefProxy": StructRefProxy,
201
+ struct_type_class.__name__: struct_type_class
202
+ }
203
+ }
204
+ code = compile(code_txt, getfile(_file_anchor), mode="exec")
205
+ exec(code, ns)
206
+ return ns[struct_name]
36
207
 
37
208
 
38
209
  def prune_type(ty):
@@ -0,0 +1,17 @@
1
+ import inspect
2
+
3
+
4
+ def make_params_strings(func):
5
+ func_params = inspect.signature(func).parameters
6
+ func_params_str = ', '.join(
7
+ [k if v.default == inspect._empty else f'{k}={v.default}' for k, v in func_params.items()]
8
+ )
9
+ func_names_params_str = ', '.join(func_params.keys())
10
+ return func_params_str, func_names_params_str
11
+
12
+
13
+ if __name__ == "__main__":
14
+ def aux(x, y, z=1):
15
+ pass
16
+ print(make_params_strings(aux))
17
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: numbox
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Author: Mikhail Goykhman
5
5
  License: MIT License (with Citation Clause)
6
6
 
@@ -1,4 +1,4 @@
1
- numbox/__init__.py,sha256=8f4ummWTPl6PtdE9DzSUgZhPjGioA_NDC-hDpqzB9qI,22
1
+ numbox/__init__.py,sha256=e3cfQy_iVs9ILsymUalJnJu5D_dd7HuUVoVwKKurcL0,22
2
2
  numbox/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  numbox/core/configurations.py,sha256=0bCmxXL-QMwtvyIDhpXLeT-1KJMf_QpH0wLuEvYLGxQ,68
4
4
  numbox/core/any/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -13,11 +13,11 @@ numbox/core/bindings/call.py,sha256=LrSsp-b4Mz0Zjg7H3XkThEIOEqVcrcViEjI9yqhuMV4,
13
13
  numbox/core/bindings/signatures.py,sha256=OcSBDpJ422eoWkJXxHPEanMNbVB7bq9f5bRq5LGr86w,479
14
14
  numbox/core/bindings/utils.py,sha256=aRtN8oUYBk9vgoUGaUJosGx0Za-vvCNwwbZg_g_-LRs,460
15
15
  numbox/core/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- numbox/core/proxy/proxy.py,sha256=kGYlEdLK40lxxu56e_S32s9YuQ6AuQnFegt5uQrUw5w,3889
16
+ numbox/core/proxy/proxy.py,sha256=Wt7yzswDmeQXt0yjcTcnLi2coneowSHWXy_IFpZZJMU,3612
17
17
  numbox/core/work/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- numbox/core/work/builder.py,sha256=tfrizyZuBy21zsoILELzQIj-wtYAuPitDqtVg7V1atw,5362
18
+ numbox/core/work/builder.py,sha256=U7hxopSwOc1ke1Hqup6xCliO7bqjq91zVX1SixYPxoM,6072
19
19
  numbox/core/work/combine_utils.py,sha256=qTVGke_ydzaTQ7o29DFjZWZzKjRNKb0L3yJMaR3TLII,2430
20
- numbox/core/work/explain.py,sha256=qEorYHRSe8P2MygodCYsz57UtWeRfaS27xHSw3hBgmo,1063
20
+ numbox/core/work/explain.py,sha256=ESwvsTgfe0w7UnM13yyVpVDtfJyAK2A1sNdF3RNb-jU,1200
21
21
  numbox/core/work/loader_utils.py,sha256=g83mDWidZJ8oLWP3I3rK8aGISYOO2S-w6HDgtosCyck,1572
22
22
  numbox/core/work/lowlevel_work_utils.py,sha256=TgRRcNfks0oaOXGXXr3ptafd_Xv_lpmH8sjBrJ9bPuI,5748
23
23
  numbox/core/work/node.py,sha256=CMolyoRQjG2A-pTQqZQ0kxKOYTKipWRC0mu8RWHuTUI,5096
@@ -26,13 +26,14 @@ numbox/core/work/print_tree.py,sha256=y2u7xmbHvpcA57y8PrGSqOunLNCqhgNXdVtXHqvy1M
26
26
  numbox/core/work/work.py,sha256=V_pYW0sqdYHh45ago3aMz0fG4U_UfsNLIcpBejn_ttU,14725
27
27
  numbox/core/work/work_utils.py,sha256=3q_nnBdzuxWWcdFpbRL2H0T9ZNkUgx1J1uhiZkX3YG4,1039
28
28
  numbox/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- numbox/utils/highlevel.py,sha256=gXYzLsFPKHQfGqaz-Z4DWcCQddv0dS6SKwIsM-_xjYg,1487
29
+ numbox/utils/highlevel.py,sha256=0sUVGOFAzFaGycKrXloGySLjp5EAPaf1B0AcOT1dfbw,8326
30
30
  numbox/utils/lowlevel.py,sha256=ACpf8_HyOIsobPlZ31bapkEyuCsV5dojW3AFrcKykrw,10712
31
31
  numbox/utils/meminfo.py,sha256=ykFi8Vt0WcHI3ztgMwvpn6NqaflDSQGL8tjI01jrzm0,1759
32
+ numbox/utils/standard.py,sha256=2fPrMlSXe2TG3CIfjJOT8LQkHEH86oOOj1AvwQkYCfA,450
32
33
  numbox/utils/timer.py,sha256=KkAkWOHQ72WtPjyiAzt_tF1q0DcOnCDkITTb85DvkUM,553
33
34
  numbox/utils/void_type.py,sha256=IkZsjNeAIShYJtvWbvERdHnl_mbF1rCRWiM3gp6II8U,404
34
- numbox-0.2.5.dist-info/LICENSE,sha256=YYgNvjH_p6-1NsdrIqGJnr1GUbZzA_8DxsP6vVfM6nY,1446
35
- numbox-0.2.5.dist-info/METADATA,sha256=kHcNZstr-whJ0XoJ6IcoXACjnAn33yjvJgdn4Ksx6sk,2792
36
- numbox-0.2.5.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
37
- numbox-0.2.5.dist-info/top_level.txt,sha256=A67jOkfqidCSYYm6ifjN_WZyIiR1B27fjxv6nNbPvjc,7
38
- numbox-0.2.5.dist-info/RECORD,,
35
+ numbox-0.2.7.dist-info/LICENSE,sha256=YYgNvjH_p6-1NsdrIqGJnr1GUbZzA_8DxsP6vVfM6nY,1446
36
+ numbox-0.2.7.dist-info/METADATA,sha256=J-Ash1idpJn-eUghPfVjNlVQhkbT0DHKa1D4K-A3ZTs,2792
37
+ numbox-0.2.7.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
38
+ numbox-0.2.7.dist-info/top_level.txt,sha256=A67jOkfqidCSYYm6ifjN_WZyIiR1B27fjxv6nNbPvjc,7
39
+ numbox-0.2.7.dist-info/RECORD,,
File without changes