zparser2 0.0.4__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.
zparser2/__init__.py ADDED
@@ -0,0 +1,556 @@
1
+ import re
2
+ import os
3
+ import sys
4
+ import importlib
5
+ from copy import copy
6
+ from inspect import getfullargspec
7
+
8
+ __version__ = "0.0.4"
9
+
10
+ def extracted_arg_name(arg):
11
+ if arg.startswith('--'):
12
+ return arg[2:]
13
+ elif arg.startswith('-'):
14
+ return arg[1:]
15
+ else:
16
+ return arg
17
+
18
+ def is_help(value):
19
+ return value in ('h', 'help')
20
+
21
+ def is_setting(value):
22
+ return value in ('s', 'setting')
23
+
24
+ def is_quiet(value):
25
+ return value in ('q', 'quiet')
26
+
27
+ def is_optional(arg):
28
+ return arg.startswith('-')
29
+
30
+
31
+ ENDC = '\033[0m'
32
+ BOLD = '\033[1m'
33
+ RED = '\033[91m'
34
+
35
+ RST_PARAM_RE = re.compile(r"^([\t ]*):param (.*?): (.*\n(\1[ \t]+.*\n*)*)", re.MULTILINE)
36
+
37
+
38
+ class ZExitException(Exception):
39
+ def __init__(self, exit_code):
40
+ self.exit_code = exit_code
41
+
42
+ def zexit(exit_code):
43
+ raise ZExitException(exit_code)
44
+
45
+ class ArgumentException(Exception):
46
+ def __init__(self, error_msg=None):
47
+ self.error_msg = error_msg
48
+
49
+ class Helper:
50
+ def __init__(self):
51
+ self.help = ""
52
+
53
+ def usage(self):
54
+ pass
55
+
56
+ def print_help(self, error_msg=None):
57
+ if error_msg:
58
+ print(BOLD + RED + error_msg + ENDC)
59
+ print('-'*80)
60
+ self.usage()
61
+ if error_msg:
62
+ zexit(1)
63
+
64
+ @property
65
+ def short_help(self):
66
+ try:
67
+ return self.help.strip().splitlines()[0]
68
+ except IndexError:
69
+ return ""
70
+
71
+
72
+ class ZParser(Helper):
73
+ def __init__(self, plugin_module=None):
74
+ if plugin_module is None:
75
+ plugin_module = []
76
+
77
+ super(ZParser, self).__init__()
78
+
79
+ self.plugin_module = None
80
+ self.plugins = {}
81
+ self.set_plugin_module(plugin_module)
82
+ self.settings = {}
83
+ self.quiet = False
84
+ self.runner = None
85
+
86
+ def __repr__(self):
87
+ return self.plugins
88
+
89
+ def set_plugin_module(self, plugin_module):
90
+ self.plugin_module = plugin_module
91
+
92
+ for module in self.plugin_module:
93
+ loaded = importlib.import_module(module)
94
+ if loaded.__file__ is None:
95
+ print("bin")
96
+ print(module)
97
+ continue
98
+ plugin_dir = os.path.dirname(loaded.__file__)
99
+ plugin_list = [filename[:-3] for filename in os.listdir(plugin_dir) if filename[-3:] == '.py'
100
+ and filename != '__init__.py']
101
+ # create dict entry before loading them
102
+ for plugin_name in plugin_list:
103
+ self.plugins[plugin_name] = Plugin(plugin_name)
104
+
105
+ for plugin_name in plugin_list:
106
+ # loaded_plugin = importlib.import_module("{}.{}".format(module, plugin))
107
+
108
+ try:
109
+ loaded_plugin = importlib.import_module("{}.{}".format(module, plugin_name))
110
+ except ImportError as e:
111
+ del self.plugins[plugin_name]
112
+ print("Failed to load plugin {}.{} [{}]".format(module, plugin_name, e))
113
+ raise
114
+ else:
115
+ try:
116
+ self.plugins[plugin_name].alias = loaded_plugin.alias
117
+ except AttributeError:
118
+ self.plugins[plugin_name].alias = []
119
+ self.plugins[plugin_name].help = loaded_plugin.__doc__ or ""
120
+ return self
121
+
122
+ def task(self, function=None, name=None, overwrite=None, short=None):
123
+ if overwrite is None:
124
+ overwrite = {}
125
+
126
+ if short is None:
127
+ short = {}
128
+
129
+ if not function:
130
+ return lambda function: self.task(function, name=name, overwrite=overwrite, short=short)
131
+
132
+ plugin_name = function.__module__.split('.')[-1]
133
+ self._register(plugin_name, function, name, overwrite, short)
134
+ return function
135
+
136
+ def _register(self, plugin_name, function, name=None, overwrite=None, short=None):
137
+ if overwrite is None:
138
+ overwrite = {}
139
+
140
+ if short is None:
141
+ short = {}
142
+
143
+ task = Task(function, name, overwrite, short)
144
+ if plugin_name not in self.plugins:
145
+ self.plugins[plugin_name] = Plugin(plugin_name)
146
+ self.plugins[plugin_name].alias = sys.modules[function.__module__].__dict__.get("alias", [])
147
+ self.plugins[plugin_name].help = sys.modules[function.__module__].__doc__ or ""
148
+ self.plugins[plugin_name].add_task(task)
149
+
150
+
151
+ def usage(self):
152
+ print("{} <plugin_name> <task>".format(self.prog_name))
153
+ print("Plugin list:")
154
+ for plugin in [value for (key, value) in sorted(self.plugins.items())]:
155
+ print(" {:20} - {}".format(plugin.name, plugin.short_help))
156
+
157
+ def parse(self, argv=None, prog_name=None):
158
+ if argv is None:
159
+ argv = sys.argv
160
+
161
+ self.prog_name = prog_name if prog_name else argv[0]
162
+
163
+ argv = argv[1:]
164
+ if not argv or (argv and is_optional(argv[0]) and is_help(extracted_arg_name(argv[0]))):
165
+ self.print_help()
166
+ zexit(0)
167
+
168
+ # parse global argument
169
+ argv = self.parse_global(argv)
170
+
171
+ if is_optional(argv[0]):
172
+ self.print_help("This argument is unexpected {}".format(argv[0]))
173
+
174
+ plugin = self._load_plugin_from_arg(argv[0])
175
+ self.runner = plugin.parse(argv[1:])
176
+ return self.runner
177
+
178
+ def _load_plugin_from_arg(self, arg):
179
+ for plugin in self.plugins.values():
180
+ if arg == plugin.name or arg in plugin.alias:
181
+ return plugin
182
+ return self.print_help("Plugin with name {} doesn't exist".format(arg))
183
+
184
+ def parse_global(self, argv=None):
185
+ pos = 0
186
+ need_setting_key = False
187
+ current_setting = None
188
+ for arg in argv:
189
+ if need_setting_key:
190
+ if is_optional(arg):
191
+ self.print_help("Need setting key")
192
+ else:
193
+ if current_setting:
194
+ self.settings[current_setting] = arg
195
+ need_setting_key = False
196
+ current_setting = None
197
+ else:
198
+ current_setting = arg
199
+ pos += 1
200
+
201
+ else:
202
+ if is_optional(arg):
203
+ if is_quiet(extracted_arg_name(arg)):
204
+ self.quiet = True
205
+ pos += 1
206
+ elif is_setting(extracted_arg_name(arg)):
207
+ pos += 1
208
+ need_setting_key = True
209
+ else:
210
+ self.print_help("Unexpected argument {}".format(arg))
211
+ else:
212
+ break
213
+ return argv[pos:]
214
+
215
+
216
+ class Plugin(Helper):
217
+ def __init__(self, name):
218
+ super(Plugin, self).__init__()
219
+ self.name = name
220
+ self.alias = []
221
+ self.tasks = {}
222
+ self.help = ""
223
+
224
+ def add_task(self, task):
225
+ self.tasks[task.name] = task
226
+
227
+ def __repr__(self):
228
+ return "{}".format(self.tasks)
229
+
230
+ def usage(self):
231
+ print(self.help)
232
+
233
+ print("{} {} <task>".format(z.prog_name, self.name))
234
+ print("Plugin alias: {}".format(self.alias))
235
+ print("Tasks:")
236
+ for task in [value for (key, value) in sorted(self.tasks.items())]:
237
+ print(" {:20} - {}".format(task.name, task.short_help))
238
+
239
+ def parse(self, argv=None):
240
+ if not argv:
241
+ self.print_help("You need to specify a task")
242
+ zexit(0)
243
+
244
+ arg = argv[0]
245
+ if is_optional(arg):
246
+ if is_help(extracted_arg_name(arg)):
247
+ self.print_help()
248
+ zexit(0)
249
+ else:
250
+ self.print_help("We don't expect this option here: {}".format(arg))
251
+ else:
252
+ task = self._load_task_from_arg(arg)
253
+ task.parse(argv[1:])
254
+ return task
255
+
256
+ def _load_task_from_arg(self, arg):
257
+ for task in self.tasks.values():
258
+ if arg == task.name:
259
+ return task
260
+ self.print_help("Task with name {} doesn't exist".format(arg))
261
+
262
+
263
+ class Task(Helper):
264
+ def __init__(self, function, name, overwrite, short):
265
+ super(Task, self).__init__()
266
+ if name is None:
267
+ name = function.__name__
268
+ self.function = function
269
+ self.name = name
270
+ self.overwrite = overwrite
271
+ self.short = short
272
+ self.args = []
273
+ self.optional_args = []
274
+ self.varargs = None
275
+ self.annotations = {}
276
+ self._init_args()
277
+ self._use_docstring()
278
+
279
+ @property
280
+ def all_args(self):
281
+ return self.args + self.optional_args
282
+
283
+ def _use_docstring(self):
284
+ docstring = self.function.__doc__ or ""
285
+ for match in RST_PARAM_RE.finditer(docstring):
286
+ name = match.group(2)
287
+ value = match.group(3)
288
+ for arg in self.all_args:
289
+ if arg.arg_python == name:
290
+ arg.help = value
291
+ docstring = RST_PARAM_RE.sub("", docstring)
292
+ self.help = docstring
293
+
294
+ def _init_args(self):
295
+ argdata = getfullargspec(self.function)
296
+
297
+ args = argdata.args
298
+ defaults = argdata.defaults
299
+
300
+
301
+ args2 = copy(args)
302
+ if defaults:
303
+ for arg, default in zip(args[0 - len(defaults):], defaults):
304
+ args2.remove(arg)
305
+ short = self.short.get(arg, None)
306
+ name = self.overwrite.get(arg, None)
307
+ self.optional_args.append(ArgumentOptional(arg, default, name, short))
308
+ for arg in args2:
309
+ self.args.append(Argument(arg))
310
+
311
+ if argdata.varargs:
312
+ self.varargs = Varargs(argdata.varargs)
313
+
314
+ if argdata.annotations:
315
+ self.annotations = argdata.annotations
316
+
317
+
318
+ def _clean_args(self):
319
+ for arg in self.optional_args:
320
+ arg.is_set = False
321
+ if self.varargs:
322
+ self.varargs.value = []
323
+
324
+ def parse(self, argv=None):
325
+ self._clean_args()
326
+ if argv and is_optional(argv[0]) and is_help(extracted_arg_name(argv[0])):
327
+ self.print_help()
328
+ zexit(0)
329
+ arg_pos = 0
330
+ if not argv and not self.args:
331
+ pass
332
+ elif len(argv) < len(self.args):
333
+ self.print_help("You need to specify the required arguments {}".format(self.args))
334
+ else:
335
+ current_arg = None
336
+ in_optional = False
337
+ for value in argv:
338
+ if arg_pos < len(self.all_args):
339
+ valid = False
340
+ if not is_optional(value):
341
+ if current_arg:
342
+ current_arg.value = value
343
+ current_arg = None
344
+ else:
345
+ if in_optional:
346
+ self.print_help("Error we don't expect a positional argument after an optional "
347
+ "declaration")
348
+ self.all_args[arg_pos].value = value
349
+ arg_pos += 1
350
+ valid = True
351
+ else:
352
+ if current_arg:
353
+ self.print_help("Except value for: {}".format(current_arg.name))
354
+ arg_name = extracted_arg_name(value)
355
+ for argument in self.optional_args:
356
+ if argument.name == arg_name or argument.short == arg_name:
357
+ if not isinstance(argument.default, bool):
358
+ current_arg = argument
359
+ valid = True
360
+ else:
361
+ argument.value = not argument.default
362
+ valid = True
363
+ in_optional = True
364
+ if not valid:
365
+ self.print_help("Invalid argument {}".format(value))
366
+ else:
367
+ if self.varargs:
368
+ self.varargs.value.append(value)
369
+ else:
370
+ self.print_help("too many arguments")
371
+ if arg_pos < len(self.args):
372
+ self.print_help("You need to give data for each positional argument")
373
+
374
+ # set optional argument not set to default
375
+ for arg in self.optional_args:
376
+ arg.use_default()
377
+
378
+ @property
379
+ def plugin(self):
380
+ return self.function.__module__.split('.')[-1]
381
+
382
+ def usage(self):
383
+ print(self.help)
384
+ print("Usage:")
385
+ parameters = []
386
+ for arg in self.all_args:
387
+ if isinstance(arg, ArgumentOptional):
388
+ if isinstance(arg.default, bool):
389
+ parameter = "[--{p}]"
390
+ else:
391
+ parameter = "[--{p} {p}]"
392
+ else:
393
+ parameter = "{p}"
394
+ parameters.append(parameter.format(p=arg.name))
395
+ if self.varargs:
396
+ parameters.append("[{p}, [{p}...]".format(p=self.varargs.name))
397
+
398
+ print(" {} {} {} {}".format(z.prog_name, self.plugin, self.name, " ".join(parameters)))
399
+ if self.args:
400
+ print("Positional arguments:")
401
+ for arg in self.args:
402
+ print(" {} - {} {}".format(arg.name, arg.short_help, self.annotations.get(arg.name, "")))
403
+
404
+ if self.optional_args:
405
+ print("Optional arguments:")
406
+ for arg in self.optional_args:
407
+ arg_name = "--{}".format(arg.name)
408
+ if arg.short:
409
+ arg_name = "{}/-{}".format(arg_name, arg.short)
410
+ print(" {} (Default: {}) {} - {}".format(arg_name, arg.default, self.annotations.get(arg.name, ""), arg.short_help))
411
+
412
+ if self.varargs:
413
+ print(" {} - {}".format(self.varargs.name, self.varargs.short_help))
414
+
415
+ def _args_value(self):
416
+ only_string_parameters = [arg.value for arg in self.all_args] + ([] if not self.varargs else self.varargs.value)
417
+ parsed_paramenters = self._parse_floats_and_ints_in_a_list(only_string_parameters)
418
+ self._enforce_variable_annotations(parsed_paramenters)
419
+ return parsed_paramenters
420
+
421
+ def _enforce_variable_annotations(self, parsed_paramenters):
422
+ if len(parsed_paramenters) <= len(self.all_args):
423
+ for i in range(len(parsed_paramenters)):
424
+ param_name = str(self.all_args[i])
425
+ param_class = self.annotations.get(param_name)
426
+ if param_class is not None:
427
+ param_value = parsed_paramenters[i]
428
+ if isinstance(param_value, param_class) or param_value.__class__ == int and param_class == float:
429
+ continue
430
+ else:
431
+ raise ArgumentException(f"Invalid value for paramter {param_name}. A {param_class} is expected, not {param_value.__class__}")
432
+
433
+ def _parse_floats_and_ints_in_a_list(self, the_list):
434
+ size = len(the_list)
435
+ for i in range(0, size):
436
+ elem = the_list[i]
437
+ if isinstance(elem, str):
438
+ if "." in elem:
439
+ try:
440
+ new_elem = float(elem)
441
+ except ValueError:
442
+ continue
443
+ else:
444
+ try:
445
+ new_elem = int(elem)
446
+ except ValueError:
447
+ continue
448
+ the_list[i] = new_elem
449
+
450
+ return the_list
451
+
452
+ def run(self):
453
+ try:
454
+ result = self.function(*self._args_value())
455
+ except ArgumentException as e:
456
+ self.print_help(e.error_msg)
457
+ else:
458
+ if result or result is False:
459
+ if isinstance(result, list):
460
+ if isinstance(result[0], str):
461
+ x = []
462
+ for r in result:
463
+ if " " in r:
464
+ x.append('"{}"'.format(r))
465
+ else:
466
+ x.append(r)
467
+ output = " ".join(x)
468
+ print(output)
469
+ else:
470
+ for r in result:
471
+ print(r)
472
+ elif isinstance(result, dict):
473
+ for key, value in result.iteritems():
474
+ print("{}\t{}".format(key, value))
475
+ else:
476
+ print(result)
477
+
478
+ def __repr__(self):
479
+ return self.name
480
+
481
+
482
+ class Argument(Helper):
483
+ def __init__(self, arg_python, name=None, short=None):
484
+ super(Argument, self).__init__()
485
+ self.arg_python = arg_python
486
+ self.name = name or arg_python
487
+ self.short = short
488
+ self._value = None
489
+
490
+ @property
491
+ def value(self):
492
+ return self._value
493
+
494
+ @value.setter
495
+ def value(self, value):
496
+ self._value = value
497
+
498
+ def __repr__(self):
499
+ return self.name
500
+
501
+
502
+ class ArgumentOptional(Argument):
503
+ def __init__(self, arg_python, default, name=None, short=None):
504
+ if isinstance(default, bool) and default:
505
+ if not name:
506
+ name = "no-{}".format(arg_python)
507
+
508
+ super(ArgumentOptional, self).__init__(arg_python, name, short)
509
+ self.default = default
510
+ self.is_set = False
511
+
512
+ def use_default(self):
513
+ if not self.is_set:
514
+ self._value = self.default
515
+
516
+ @Argument.value.getter
517
+ def value(self):
518
+ if type(self._value) == type(self.default):
519
+ return self._value
520
+ if isinstance(self.default, bool):
521
+ if self._value.lower() in ['true', '1']:
522
+ return True
523
+ elif self._value.lower() in ['false', '0']:
524
+ return False
525
+ else:
526
+ self.print_help("Invalid data, expect boolean for arg: {}".format(self.name))
527
+ elif isinstance(self.default, int):
528
+ return int(self._value)
529
+ elif isinstance(self.default, list):
530
+ result = []
531
+ for i in self._value.split(','):
532
+ result.append(i)
533
+ return result
534
+ return self._value
535
+
536
+ @value.setter
537
+ def value(self, value):
538
+ self.is_set = True
539
+ self._value = value
540
+
541
+
542
+ class Varargs(Argument):
543
+ def __init__(self, arg_python, name=None, short=None):
544
+ super(Varargs, self).__init__(arg_python, name, short)
545
+ self._value = []
546
+
547
+
548
+ def init(plugin_list: list=[]) -> int:
549
+ global z
550
+ try:
551
+ z.set_plugin_module(plugin_list).parse().run()
552
+ except ZExitException as exit_exception:
553
+ sys.exit(exit_exception.exit_code)
554
+ sys.exit(0)
555
+
556
+ z = ZParser()
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: zparser2
3
+ Version: 0.0.4
4
+ Summary: ZParser Cli Argument Library. Nothing comes after Z
5
+ Author: Marcos Diez, Benjamin Port
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/marcosdiez/zparser2
8
+ Project-URL: Issues, https://github.com/marcosdiez/zparser2/issues
9
+ Keywords: cli,argument,parsing
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Environment :: Console
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ ZParser2 Cli Argument Parsing Library
22
+
23
+
24
+ `zparser2` is probably the simplest most opinionated argument parsing library. Lot's of conventions, zero configuration!
25
+
26
+ If you add the `@z.task` notation to a function, it automatically become available to the CLI.
27
+ The function's parameters are what the CLI will expect. If notations are given, type will be enforced.
28
+ The file in which the function is located will be the module in which the function will be available.
29
+
30
+ The downside is that you can only have two layers in your cli. That being said, more than that would be too complex and less than that you don't really need a library.
31
+
32
+
33
+ Example
34
+ -------
35
+
36
+ Let's say you have 3 files:
37
+
38
+
39
+ math_functions.py
40
+ ```
41
+ """here we do math"""
42
+ from zparser2 import z
43
+
44
+ @z.task
45
+ def duplicate_number(x: float):
46
+ """returns twice the value of x"""
47
+ return 2*x
48
+
49
+ @z.task
50
+ def triple_number(x: float):
51
+ """returns 3 times the value of x"""
52
+ return 3*x
53
+ ```
54
+
55
+ string_functions.py
56
+ ```
57
+ """string processing"""
58
+ from zparser2 import z
59
+
60
+ @z.task
61
+ def add_square_brackets_to_string(x: str):
62
+ """x -> [x]"""
63
+ return f"[{x}]"
64
+
65
+ @z.task
66
+ def first_word(x: str):
67
+ """returns the first word of a string"""
68
+ return x.split(" ")[0]
69
+
70
+ @z.task
71
+ def last_word(x: str):
72
+ """returns the first word of a string"""
73
+ return x.split(" ")[-1]
74
+
75
+ @z.task
76
+ def another_task(somestring: str, some_int: int, workdir=None, root_url=None):
77
+ """description of the task"""
78
+ print(f"somestring={somestring}")
79
+ print(f"some_int={some_int}")
80
+ print(f"workdir={workdir}")
81
+ print(f"root_url={root_url}")
82
+ ```
83
+
84
+
85
+ mycli.py
86
+ ```
87
+ #!/usr/bin/env python3
88
+ import zparser2
89
+
90
+ import math_functions
91
+ import string_functions
92
+
93
+ if __name__ == "__main__":
94
+ zparser2.init()
95
+ ```
96
+
97
+ Output
98
+ ------
99
+
100
+ ```
101
+ (env2) mdiez@batman:~/code/zparser2/example$ ./mycli.py
102
+ ./mycli.py <plugin_name> <task>
103
+ Plugin list:
104
+ math_functions - here we do math
105
+ string_functions - string processing
106
+
107
+ ```
108
+
109
+ ```
110
+ (env2) mdiez@batman:~/code/zparser2/example$ ./mycli.py string_functions
111
+ You need to specify a task
112
+ --------------------------------------------------------------------------------
113
+ string processing
114
+ ./mycli.py string_functions <task>
115
+ Plugin alias: []
116
+ Tasks:
117
+ add_square_brackets_to_string - x -> [x]
118
+ another_task - description of the task
119
+ first_word - returns the first word of a string
120
+ last_word - returns the first word of a string
121
+ ```
122
+
123
+ ```
124
+ (env2) mdiez@batman:~/code/zparser2/example$ ./mycli.py string_functions another_task
125
+ You need to specify the required arguments [somestring, some_int]
126
+ --------------------------------------------------------------------------------
127
+ description of the task
128
+ Usage:
129
+ ./mycli.py string_functions another_task somestring some_int [--workdir workdir] [--root_url root_url]
130
+ Positional arguments:
131
+ somestring - <class 'str'>
132
+ some_int - <class 'int'>
133
+ Optional arguments:
134
+ --workdir (Default: None) -
135
+ --root_url (Default: None) -
136
+ ```
137
+
138
+ ```
139
+ (env2) mdiez@batman:~/code/zparser2/example$ ./mycli.py string_functions another_task blah 42 --root_url https://blah.com
140
+ somestring=blah
141
+ some_int=42
142
+ workdir=None
143
+ root_url=https://blah.com
144
+ ```
145
+
146
+
147
+ How to build & publish
148
+ ----------------------
149
+
150
+ * `python3 -m build`
151
+ * `python3 -m twine upload --repository testpypi dist/*`
152
+
@@ -0,0 +1,6 @@
1
+ zparser2/__init__.py,sha256=krPpaknHsX7nzMBI10YxtWdUoozAh7klu062ZBj76MM,18648
2
+ zparser2-0.0.4.dist-info/licenses/LICENSE,sha256=92QUHxkasaca-RngEOgNU0cOIutYRU7rClN4EW6pcUw,1459
3
+ zparser2-0.0.4.dist-info/METADATA,sha256=nZEW-UVP1TbK-x5yIoeN7j0Mdrxgf961AZGFEGWMXRk,4020
4
+ zparser2-0.0.4.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
5
+ zparser2-0.0.4.dist-info/top_level.txt,sha256=9TEheEFvnkIxn3Z7zBjc-TBPwD5gPqa1b5awlHKV-DY,9
6
+ zparser2-0.0.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,11 @@
1
+ Copyright 2025 Marcos Diez
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ zparser2