PyProd 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pyprod/main.py CHANGED
@@ -14,6 +14,13 @@ parser = argparse.ArgumentParser(
14
14
  description="""PyProd - More makable than make""",
15
15
  )
16
16
 
17
+ parser.add_argument(
18
+ "-C",
19
+ "--directory",
20
+ dest="directory",
21
+ help="Change to DIRECTORY before performing any operations",
22
+ )
23
+
17
24
  parser.add_argument(
18
25
  "-f", "--file", help="Use FILE as the Prodfile (default: 'PRODFILE.py')"
19
26
  )
@@ -27,12 +34,8 @@ parser.add_argument(
27
34
  )
28
35
 
29
36
  parser.add_argument(
30
- "-C",
31
- "--directory",
32
- dest="directory",
33
- help="Change to DIRECTORY before performing any operations",
37
+ "-r", "--rebuild", dest="rebuild", action="store_true", help="Rebuild all"
34
38
  )
35
-
36
39
  parser.add_argument(
37
40
  "-v",
38
41
  dest="verbose",
@@ -55,8 +58,13 @@ def print_exc(e):
55
58
  logger.exception("Terminated by exception")
56
59
 
57
60
 
61
+ def init_args(args=None):
62
+ args = pyprod.args = parser.parse_args(args)
63
+ return args
64
+
65
+
58
66
  def main():
59
- args = pyprod.args = parser.parse_args()
67
+ args = init_args()
60
68
  pyprod.verbose = args.verbose
61
69
  chdir = args.directory
62
70
  if chdir:
@@ -89,6 +97,7 @@ def main():
89
97
 
90
98
  params = {}
91
99
  targets = []
100
+
92
101
  for target in args.targets:
93
102
  if "=" in target:
94
103
  name, value = target.split("=", 1)
@@ -107,7 +116,10 @@ def main():
107
116
  sys.exit("No default target")
108
117
  targets = [target]
109
118
 
110
- ret = asyncio.run(prod.start(targets))
119
+ ret = 0
120
+ for target in targets:
121
+ ret += asyncio.run(prod.start([target]))
122
+
111
123
  if not ret:
112
124
  print(f"Nothing to be done for {targets}")
113
125
 
pyprod/prod.py CHANGED
@@ -37,6 +37,10 @@ class RuleError(Exception):
37
37
  pass
38
38
 
39
39
 
40
+ class TargetError(Exception):
41
+ pass
42
+
43
+
40
44
  class HandledExceptionError(Exception):
41
45
  pass
42
46
 
@@ -109,7 +113,7 @@ def glob(path, dir="."):
109
113
 
110
114
  def rule_to_re(rule):
111
115
  if not isinstance(rule, (str, Path)):
112
- raise RuleError(rule)
116
+ raise TypeError(f"str or Path required: {rule}")
113
117
 
114
118
  srule = str(rule)
115
119
  srule = translate(srule)
@@ -156,47 +160,48 @@ def _check_pattern(pattern):
156
160
  raise RuleError(f"{pattern}: Pattern should contain a '%'.")
157
161
 
158
162
 
159
- def _strip_dot(path):
160
- if not path:
161
- return path
162
- path = Path(path) # ./aaa/ -> aaa
163
- parts = path.parts
164
- if ".." in parts:
165
- raise RuleError(f"{path}: '..' directory is not allowed")
166
- return str(path)
167
-
168
-
169
163
  def _check_wildcard(path):
170
164
  if "*" in path:
171
165
  raise RuleError(f"{path}: '*' directory is not allowed")
172
166
 
173
167
 
174
- class Rule:
175
- def __init__(self, targets, pattern, depends, uses, builder=None):
176
- self.targets = []
177
- for target in flatten(targets or ()):
178
- target = str(target)
179
- _check_pattern_count(target)
180
- target = _strip_dot(target)
181
- target = rule_to_re(target)
182
- self.targets.append(target)
168
+ def _name_to_str(name):
169
+ match name:
170
+ case Task():
171
+ return name.name
172
+ case _TaskFunc():
173
+ return name.name
174
+ case Path():
175
+ return str(name)
176
+ case str():
177
+ return name
183
178
 
184
- self.first_target = None
185
- for target in flatten(targets or ()):
186
- target = str(target)
187
- if not target:
188
- continue
179
+ return name
189
180
 
190
- if "*" in target:
191
- continue
192
181
 
193
- if _check_pattern_count(target) == 0:
194
- # not contain one %
195
- self.first_target = target
196
- break
182
+ class Rule:
183
+ def __init__(self, targets, pattern=None, depends=(), uses=(), builder=None):
184
+ self.targets = []
185
+ self.first_target = None
186
+ if targets:
187
+ for target in flatten(targets):
188
+ if not target:
189
+ continue
190
+ target = str(target)
191
+ if not target:
192
+ continue
193
+
194
+ if not self.first_target:
195
+ if "*" not in target:
196
+ if _check_pattern_count(target) == 0:
197
+ # not contain one %
198
+ self.first_target = target
199
+
200
+ target = rule_to_re(target)
201
+ self.targets.append(target)
197
202
 
198
203
  if pattern:
199
- pattern = _strip_dot(pattern)
204
+ pattern = str(pattern)
200
205
  if _check_pattern_count(pattern) != 1:
201
206
  raise RuleError(f"{pattern}: Pattern should contain a '%'")
202
207
 
@@ -206,18 +211,16 @@ class Rule:
206
211
 
207
212
  self.depends = []
208
213
  for depend in flatten(depends or ()):
209
- depend = str(depend)
214
+ depend = _name_to_str(depend)
210
215
  _check_pattern_count(depend)
211
216
  _check_wildcard(depend)
212
- depend = _strip_dot(depend)
213
217
  self.depends.append(depend)
214
218
 
215
219
  self.uses = []
216
220
  for use in flatten(uses or ()):
217
- use = str(use)
221
+ use = _name_to_str(use)
218
222
  _check_pattern_count(use)
219
223
  _check_wildcard(use)
220
- use = _strip_dot(use)
221
224
  self.uses.append(use)
222
225
 
223
226
  self.builder = builder
@@ -227,6 +230,49 @@ class Rule:
227
230
  return f
228
231
 
229
232
 
233
+ class _TaskFunc:
234
+ def __init__(self, f, name):
235
+ self.f = f
236
+ self.name = name
237
+
238
+ def __call__(self, *args, **kwargs):
239
+ return self.f(*args, **kwargs)
240
+
241
+
242
+ def default_builder(self, *args, **kwargs):
243
+ # default builder
244
+ pass
245
+
246
+
247
+ class Task(Rule):
248
+ def __init__(self, name, depends, uses, func=None):
249
+ super().__init__((), pattern=None, depends=depends, uses=uses, builder=func)
250
+ self.name = _name_to_str(name)
251
+ if name:
252
+ self.targets = [name]
253
+ self.first_target = self.name
254
+ if func:
255
+ self._set_funcname(func)
256
+ if not self.builder:
257
+ self.builder = default_builder
258
+
259
+ def _set_funcname(self, f):
260
+ if not self.name:
261
+ if not f.__name__ or f.__name__ == "<lambda>":
262
+ raise RuleError(
263
+ "Task function should have a name. Use @task(name='name')"
264
+ )
265
+ self.name = f.__name__
266
+ self.targets = [f.__name__]
267
+
268
+ self.first_target = self.name
269
+
270
+ def __call__(self, f):
271
+ self.builder = f
272
+ self._set_funcname(f)
273
+ return _TaskFunc(f, self.name)
274
+
275
+
230
276
  class Rules:
231
277
  def __init__(self):
232
278
  self.rules = []
@@ -234,40 +280,52 @@ class Rules:
234
280
  self._detect_loop = set()
235
281
  self.frozen = False
236
282
 
237
- def add_rule(self, targets, pattern, depends, uses, builder):
283
+ def add_rule(self, targets, pattern=None, depends=(), uses=(), builder=None):
284
+ if builder:
285
+ if not callable(builder):
286
+ raise ValueError(f"{builder} is not callable")
287
+
238
288
  if self.frozen:
239
289
  raise RuntimeError("No new rule can be added after initialization")
240
290
 
241
- dep = Rule(
242
- targets,
243
- pattern,
244
- depends,
245
- uses,
246
- builder,
247
- )
291
+ dep = Rule(targets, pattern, depends, uses, builder)
248
292
  self.rules.append(dep)
249
293
  return dep
250
294
 
251
- def rule(self, target, pattern=None, depends=(), uses=()):
252
- if (not isinstance(depends, Collection)) or isinstance(depends, str):
253
- depends = [depends]
254
- if (not isinstance(uses, Collection)) or isinstance(uses, str):
255
- uses = [uses]
256
- dep = self.add_rule([target], pattern, depends, uses, None)
295
+ def add_task(self, name=None, depends=(), uses=(), func=None):
296
+ if self.frozen:
297
+ raise RuntimeError("No new rule can be added after initialization")
298
+ dep = Task(name, depends, uses, func)
299
+ self.rules.append(dep)
300
+ return dep
301
+
302
+ def rule(self, targets, *, pattern=None, depends=(), uses=()):
303
+ if not targets:
304
+ raise ValueError("No target specified")
305
+
306
+ dep = self.add_rule([targets], pattern, depends, uses, None)
307
+ return dep
308
+
309
+ def task(self, func=None, *, name=None, depends=(), uses=()):
310
+ if func:
311
+ if not callable(func):
312
+ raise ValueError(f"{func} is not callable")
313
+
314
+ dep = self.add_task(name, depends, uses, func)
257
315
  return dep
258
316
 
259
317
  def iter_rule(self, name):
260
- name = str(name)
318
+ name = _name_to_str(name)
261
319
  for dep in self.rules:
262
320
  for target in dep.targets:
263
- m = re.fullmatch(str(target), name)
321
+ m = re.fullmatch(target, name)
264
322
  if m:
265
323
  stem = None
266
324
  d = m.groupdict().get("stem", None)
267
325
  if d is not None:
268
326
  stem = d
269
327
  elif dep.pattern:
270
- m = re.fullmatch(str(dep.pattern), name)
328
+ m = re.fullmatch(dep.pattern, name)
271
329
  if m:
272
330
  stem = m.groupdict().get("stem", None)
273
331
 
@@ -282,7 +340,6 @@ class Rules:
282
340
  break
283
341
 
284
342
  def get_dep_names(self, name):
285
- assert name
286
343
  ret_depends = []
287
344
  ret_uses = []
288
345
 
@@ -307,11 +364,13 @@ class Rules:
307
364
  return depends, uses, dep
308
365
 
309
366
  def build_tree(self, name, lv=1):
367
+ assert name
310
368
  self.frozen = True
311
369
 
312
- name = str(name)
370
+ name = _name_to_str(name)
313
371
  if name in self._detect_loop:
314
- raise CircularReferenceError(name)
372
+ raise CircularReferenceError(f"Circular reference detected: {name}")
373
+
315
374
  self._detect_loop.add(name)
316
375
  try:
317
376
  if name in self.tree:
@@ -326,7 +385,7 @@ class Rules:
326
385
  depends.extend(build_uses)
327
386
 
328
387
  depends = unique_list(depends)
329
- self.tree[name].update(str(d) for d in depends)
388
+ self.tree[name].update(depends)
330
389
  for dep in depends:
331
390
  self.build_tree(dep, lv=lv + 1)
332
391
 
@@ -339,17 +398,19 @@ class Checkers:
339
398
  self.checkers = []
340
399
 
341
400
  def get_checker(self, name):
401
+ name = _name_to_str(name)
342
402
  for targets, f in self.checkers:
343
403
  for target in targets:
344
- if fnmatch(name, str(target)):
404
+ if fnmatch(name, target):
345
405
  return f
346
406
 
347
- def add_check(self, target, f):
348
- self.checkers.append((list(t for t in flatten(target)), f))
407
+ def add_check(self, targets, f):
408
+ targets = list(map(_name_to_str, flatten(targets or ())))
409
+ self.checkers.append((targets, f))
349
410
 
350
- def check(self, target):
411
+ def check(self, targets):
351
412
  def deco(f):
352
- self.add_check(target, f)
413
+ self.add_check(targets, f)
353
414
  return f
354
415
 
355
416
  return deco
@@ -421,22 +482,27 @@ def write(filename, s, append=False):
421
482
  f.write(s)
422
483
 
423
484
 
424
- def quote(s):
425
- return shlex.quote(str(s))
426
-
427
-
428
- def squote(*s):
485
+ def quote(*s):
429
486
  ret = [shlex.quote(str(x)) for x in flatten(s)]
430
487
  return ret
431
488
 
432
489
 
490
+ def squote(s):
491
+ s = " ".join(str(e) for e in flatten(s))
492
+ return shlex.quote(s)
493
+
494
+
433
495
  def makedirs(path):
434
496
  os.makedirs(path, exist_ok=True)
435
497
 
436
498
 
437
499
  class Prod:
438
500
  def __init__(self, modulefile, njobs=1, params=None):
439
- self.modulefile = Path(modulefile)
501
+ if modulefile:
502
+ self.modulefile = Path(modulefile)
503
+ else:
504
+ self.modulefile = None
505
+
440
506
  self.rules = Rules()
441
507
  self.checkers = Checkers()
442
508
  if njobs > 1:
@@ -445,11 +511,14 @@ class Prod:
445
511
  self.executor = None
446
512
  self.params = Params(params)
447
513
  self.buildings = {}
448
- self.module = self.load_pyprodfile(self.modulefile)
514
+ self.module = None
515
+ if self.modulefile:
516
+ self.module = self.load_pyprodfile(self.modulefile)
449
517
  self.built = 0 # number of build execused
450
518
 
451
519
  def get_module_globals(self):
452
520
  globals = {
521
+ "build": self.build,
453
522
  "capture": capture,
454
523
  "check": self.checkers.check,
455
524
  "environ": Envs(),
@@ -466,6 +535,7 @@ class Prod:
466
535
  "rule": self.rules.rule,
467
536
  "run": run,
468
537
  "shutil": shutil,
538
+ "task": self.rules.task,
469
539
  "write": write,
470
540
  "MAX_TS": MAX_TS,
471
541
  "Path": Path,
@@ -489,47 +559,28 @@ class Prod:
489
559
  async def run_in_executor(self, func, *args, **kwargs):
490
560
  if self.executor:
491
561
  loop = asyncio.get_running_loop()
492
- return await loop.run_in_executor(
562
+ ret = await loop.run_in_executor(
493
563
  self.executor, lambda: func(*args, **kwargs)
494
564
  )
495
565
  else:
496
- return func(*args, **kwargs)
566
+ ret = func(*args, **kwargs)
567
+
568
+ return ret
497
569
 
498
570
  def get_default_target(self):
499
571
  return self.rules.select_first_target()
500
572
 
501
573
  async def start(self, deps):
574
+ self.loop = asyncio.get_running_loop()
502
575
  self.built = 0
503
- names = []
504
- for name in deps:
505
- if isinstance(name, str):
506
- value = getattr(self.module, name, None)
507
- if value:
508
- names.append(value)
509
- else:
510
- names.append(name)
511
- else:
512
- names.append(name)
513
-
514
- builds = []
515
- waitings = []
516
- for obj in flatten(names):
517
- if isinstance(obj, str | Path):
518
- builds.append(obj)
519
- elif isinstance(obj, Rule):
520
- raise RuleError(obj)
521
- elif callable(obj):
522
- self.built += 1
523
- task = asyncio.create_task(self.run_in_executor(obj))
524
- waitings.append(task)
525
- else:
526
- raise RuleError(obj)
576
+ self.deps = deps[:]
577
+ while self.deps:
578
+ dep = self.deps.pop(0)
579
+ await self.schedule([dep])
527
580
 
528
- await self.build(builds)
529
- await asyncio.gather(*waitings)
530
581
  return self.built
531
582
 
532
- async def build(self, deps):
583
+ async def schedule(self, deps):
533
584
  tasks = []
534
585
  waits = []
535
586
  for dep in deps:
@@ -581,9 +632,11 @@ class Prod:
581
632
  ret = MAX_TS
582
633
  return Exists(name, True, ret)
583
634
 
584
- async def run(self, name): # -> Any | int:
585
- name = str(name)
635
+ def build(self, *deps):
636
+ self.deps[0:0] = [_name_to_str(name) for name in flatten(deps)]
586
637
 
638
+ async def run(self, name): # -> Any | int:
639
+ name = _name_to_str(name)
587
640
  self.rules.build_tree(name)
588
641
  deps, uses = self.rules.get_dep_names(name)
589
642
  selected = self.rules.select_builder(name)
@@ -594,9 +647,14 @@ class Prod:
594
647
 
595
648
  ts = 0
596
649
  if deps:
597
- ts = await self.build(deps)
650
+ ts = await self.schedule(deps)
598
651
  if uses:
599
- await self.build(uses)
652
+ await self.schedule(uses)
653
+
654
+ if selected and isinstance(builder, Task):
655
+ self.built += 1
656
+ await self.run_in_executor(builder.builder, *build_deps)
657
+ return MAX_TS
600
658
 
601
659
  exists = await self.is_exists(name)
602
660
 
@@ -608,8 +666,14 @@ class Prod:
608
666
  logger.debug("%r already exists", name)
609
667
 
610
668
  if not exists.exists and not selected:
611
- raise NoRuleToMakeTargetError(name)
612
- elif selected and ((not exists.exists) or (ts >= MAX_TS) or (exists.ts < ts)):
669
+ raise NoRuleToMakeTargetError(f"No rule to make target: {name}")
670
+
671
+ elif selected and (
672
+ (not exists.exists)
673
+ or (ts >= MAX_TS)
674
+ or (exists.ts < ts)
675
+ or pyprod.args.rebuild
676
+ ):
613
677
  logger.warning("building: %r", name)
614
678
  await self.run_in_executor(builder.builder, name, *build_deps)
615
679
  self.built += 1
pyprod/utils.py CHANGED
@@ -1,8 +1,8 @@
1
- from collections.abc import Iterable, Sequence
1
+ from collections.abc import Iterable
2
2
 
3
3
 
4
4
  def flatten(seq):
5
- if isinstance(seq, str) or (not isinstance(seq, Sequence)):
5
+ if isinstance(seq, str) or (not isinstance(seq, Iterable)):
6
6
  yield seq
7
7
  return
8
8
 
pyprod/venv.py CHANGED
@@ -11,15 +11,15 @@ import pyprod
11
11
  from .utils import flatten
12
12
 
13
13
  THREADID = threading.get_ident()
14
- pyprodenv = ".pyprod"
15
14
  venvdir = None
16
15
 
17
- PYPRODVENV = ".pyprod"
16
+ PYPRODVENV = "pyprod"
18
17
 
19
18
 
20
19
  def makevenv(conffile):
21
20
  global venvdir
22
- venvdir = Path(".") / f".{conffile.name}{PYPRODVENV}"
21
+ major, minor = sys.version_info[:2]
22
+ venvdir = Path(".") / f".{conffile.name}.{major}.{minor}.{PYPRODVENV}"
23
23
  if not venvdir.is_dir():
24
24
  venv.main([str(venvdir)])
25
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyProd
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: PyProd: More Makeable than Make
5
5
  Project-URL: Homepage, https://github.com/atsuoishimoto/pyprod
6
6
  Project-URL: Documentation, https://pyprod.readthedocs.io/en/latest/
@@ -17,7 +17,7 @@ Description-Content-Type: text/x-rst
17
17
  PyProd - More Makeable than Make
18
18
  =================================
19
19
 
20
- PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://pyprod.readthedocs.io/en/latest/>`_.
20
+ PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://pyprod.readthedocs.io/en/stable/>`_.
21
21
 
22
22
 
23
23
  Features
@@ -42,22 +42,26 @@ With PyProd, a traditional Makefile for C can be expressed as a Python script li
42
42
  .. code-block:: python
43
43
 
44
44
  CC = "gcc"
45
- CFLAGS = "-I."
45
+ CFLAGS = "-c -I."
46
46
  DEPS = "hello.h"
47
47
  OBJS = "hello.o main.o".split()
48
+ EXE = "hello.exe"
48
49
 
49
50
  @rule("%.o", depends=("%.c", DEPS))
50
51
  def compile(target, src, *deps):
51
- run(CC, "-c -o", target, src, CFLAGS)
52
+ run(CC, "-o", target, src, CFLAGS)
52
53
 
53
- @rule("hello.exe", depends=OBJS)
54
+ @rule(EXE, depends=OBJS)
54
55
  def link(target, *objs):
55
56
  run(CC, "-o", target, objs)
56
57
 
58
+ @task
57
59
  def clean():
58
60
  run("rm -f", OBJS, "hello.exe")
59
61
 
60
- all = "hello.exe"
62
+ @task
63
+ def rebuild():
64
+ build(clean, EXE)
61
65
 
62
66
 
63
67
  To run the build script, simply execute:
@@ -67,6 +71,8 @@ To run the build script, simply execute:
67
71
  $ cd project
68
72
  $ pyprod
69
73
 
74
+ Other examples can be found in the `samples <https://github.com/atsuoishimoto/pyprod/tree/main/samples>`_ directory.
75
+
70
76
  License
71
77
  -------
72
78
  PyProd is licensed under the MIT License. See the `LICENSE <LICENSE>`_ file for more details.
@@ -0,0 +1,11 @@
1
+ pyprod/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
+ pyprod/main.py,sha256=w8xm7Dz4eXF6D6FhZuxlPfU24KpnXciJLuAMT6_y3zI,2808
4
+ pyprod/prod.py,sha256=Vu376JyNqiieCMy8jVvBml3TAf32Pq4XxSdpC4bBdgY,18424
5
+ pyprod/utils.py,sha256=hvqeZhXyVAsHAdPUG6LhBOs2dMVXuDUWBYiIPBcsoKw,391
6
+ pyprod/venv.py,sha256=lRTYxvtX876FQ9-bTYmsOHYV3nuMkKHO5hTLog0iHro,1085
7
+ pyprod-0.4.0.dist-info/METADATA,sha256=zkpPbp07U627hgqWE6yDYGe_dZEDQTCiE3FidIfQ5mM,2652
8
+ pyprod-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ pyprod-0.4.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
+ pyprod-0.4.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
+ pyprod-0.4.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- pyprod/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
- pyprod/main.py,sha256=nS0mcLy3Ix7ydnX1Pu5g7ceLzhh6DndwJWO5sjO_qWg,2580
4
- pyprod/prod.py,sha256=r2e-JSHiTw2SOcv-tYFx4aRpVkQbOb5j9vEVrjT0meE,16694
5
- pyprod/utils.py,sha256=oiiUkSbeqTazbtJ6gz7ZKqG1OvAeV-nV9u_3Y0DCOOM,401
6
- pyprod/venv.py,sha256=_riw56YQvUOSd55u_1m9ElsqPdjM5qVvIZP6dr9Fzt4,1051
7
- pyprod-0.3.0.dist-info/METADATA,sha256=t9TMYeBveYE1LFZNpcYJAtOdUo7gPARFHmIZ9p0bnag,2477
8
- pyprod-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- pyprod-0.3.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
10
- pyprod-0.3.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
11
- pyprod-0.3.0.dist-info/RECORD,,
File without changes