plain-parser 0.1.4.dev9__py3-none-any.whl → 0.1.4.dev10__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.
plain_parser/cli.py CHANGED
@@ -1,8 +1,10 @@
1
- """Command-line entry point: ``plain-parser check <file.plain> [--template-dir DIR] [--config-name NAME]``."""
1
+ """Command-line entry point: ``plain-parser {check,parse} <file.plain> [--template-dir DIR] [--config-name NAME]``."""
2
2
 
3
3
  import argparse
4
+ import json
4
5
  import os
5
6
  import sys
7
+ from dataclasses import dataclass
6
8
 
7
9
  import yaml
8
10
 
@@ -10,6 +12,7 @@ from plain_parser import loaders, plain_file, plain_spec
10
12
 
11
13
  DEFAULT_CONFIG_NAME = "config.yaml"
12
14
  TEMPLATE_DIR_CONFIG_KEYS = ("template_dir", "template-dir")
15
+ PARSE_FORMAT_VERSION = 1
13
16
 
14
17
 
15
18
  class AmbiguousConfigFileError(Exception):
@@ -20,19 +23,29 @@ def build_parser() -> argparse.ArgumentParser:
20
23
  parser = argparse.ArgumentParser(prog="plain-parser", description="Tools for ***plain specification files.")
21
24
  subparsers = parser.add_subparsers(dest="command", required=True)
22
25
 
23
- check_parser = subparsers.add_parser("check", help="Validate a .plain module and its requires chain.")
24
- check_parser.add_argument("plain_file", help="Path to the .plain module.")
25
- check_parser.add_argument(
26
- "--template-dir",
27
- help="Directory searched for modules and templates after the .plain file's own directory. "
28
- "Overrides the template-dir value of the config file.",
26
+ check_parser = subparsers.add_parser(
27
+ "check",
28
+ help="Validate a .plain module and its requires chain, and read every linked file: "
29
+ "binary files and large base64 blobs are rejected.",
29
30
  )
30
- check_parser.add_argument(
31
- "--config-name",
32
- default=DEFAULT_CONFIG_NAME,
33
- help="Name of the config file to read template-dir from. Looked up in the .plain file's directory "
34
- "and the current working directory. Defaults to %(default)s.",
31
+ parse_parser = subparsers.add_parser(
32
+ "parse",
33
+ help="Print the parse result of a .plain module and its requires chain as JSON. Linked files are "
34
+ "located but never read.",
35
35
  )
36
+ for subparser in (check_parser, parse_parser):
37
+ subparser.add_argument("plain_file", help="Path to the .plain module.")
38
+ subparser.add_argument(
39
+ "--template-dir",
40
+ help="Directory searched for modules and templates after the .plain file's own directory. "
41
+ "Overrides the template-dir value of the config file.",
42
+ )
43
+ subparser.add_argument(
44
+ "--config-name",
45
+ default=DEFAULT_CONFIG_NAME,
46
+ help="Name of the config file to read template-dir from. Looked up in the .plain file's directory "
47
+ "and the current working directory. Defaults to %(default)s.",
48
+ )
36
49
  return parser
37
50
 
38
51
 
@@ -71,23 +84,124 @@ def template_dir_from_config(config_file: str) -> str | None:
71
84
  return None
72
85
 
73
86
 
74
- def check(plain_file_path: str, template_dir: str | None) -> None:
75
- """Raise if the module, its requires chain, or its linked resources are invalid."""
87
+ @dataclass
88
+ class ParseResult:
89
+ modules: list[
90
+ tuple[str, dict]
91
+ ] # (module name, plain source tree) for the chain, deepest ancestor first, entry last.
92
+ # Each link target in the chain, as written, to the path it resolves to, relative to the entry module's dir
93
+ # with `/` as separator (loaders.relative_resource_path).
94
+ resources: dict[str, str]
95
+
96
+
97
+ def parse_spec(plain_file_path: str, template_dir: str | None, load_resources: bool) -> ParseResult:
98
+ """Parse the module and its requires chain, resolve its links, and walk the entry's FRIDs; raise on the first error.
99
+
100
+ With ``load_resources``, each module's linked files are also read and checked (text only, no large base64 blob) right
101
+ after that module's links are collected, so a resource error is reported before a FRID-walk error.
102
+ """
103
+ # Absolute, so the chain parse and the resource paths below search the same directories.
76
104
  template_dirs = [os.path.dirname(os.path.abspath(plain_file_path))]
77
105
  if template_dir:
78
- template_dirs.append(template_dir)
106
+ template_dirs.append(os.path.abspath(template_dir))
79
107
 
80
108
  chain = plain_file.parse_module_chain(os.path.basename(plain_file_path), template_dirs)
81
109
 
110
+ # The target joined as written onto its template dir (`..` kept, symlinks not resolved), made relative to the
111
+ # entry module's dir so the result does not depend on where the project sits on the machine.
112
+ resources: dict[str, str] = {}
82
113
  for module_name, plain_source_tree in chain:
83
114
  resources_list: list[dict] = []
84
115
  plain_spec.collect_linked_resources(plain_source_tree, resources_list, None, True)
85
- loaders.load_linked_resources(template_dirs, resources_list, module_name)
116
+ if load_resources:
117
+ loaders.load_linked_resources(template_dirs, resources_list, module_name)
118
+ for resource in resources_list:
119
+ target = resource["target"]
120
+ resource_dir = loaders.find_resource_dir(template_dirs, target)
121
+ resources[target] = loaders.relative_resource_path(template_dirs[0], resource_dir, target)
86
122
 
87
123
  _, top_plain_source_tree = chain[-1]
88
124
  for frid in plain_spec.get_frids(top_plain_source_tree):
89
125
  plain_spec.get_specifications_for_frid(top_plain_source_tree, frid)
90
126
 
127
+ return ParseResult(chain, resources)
128
+
129
+
130
+ def check(plain_file_path: str, template_dir: str | None) -> None:
131
+ """Raise if the module, its requires chain, or its linked resources are invalid."""
132
+ parse_spec(plain_file_path, template_dir, load_resources=True)
133
+
134
+
135
+ def _error_output(type_name: str, message: str) -> dict:
136
+ return {"format_version": PARSE_FORMAT_VERSION, "type": type_name, "message": message}
137
+
138
+
139
+ def _escape(text: str) -> str:
140
+ """The text with every non-ASCII character as a backslash escape, so it always encodes."""
141
+ return text.encode("ascii", "backslashreplace").decode("ascii")
142
+
143
+
144
+ def _encode_output(output: dict) -> bytes:
145
+ """The output as UTF-8 JSON; raise if it is not JSON-serialisable or holds text that is not valid UTF-8."""
146
+ return (json.dumps(output, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
147
+
148
+
149
+ def parse(plain_file_path: str, template_dir: str | None) -> int:
150
+ """Print the parse result, or the parser error, as one UTF-8 JSON document on stdout; return the exit code."""
151
+ output: dict
152
+ try:
153
+ result = parse_spec(plain_file_path, template_dir, load_resources=False)
154
+ output = {
155
+ "format_version": PARSE_FORMAT_VERSION,
156
+ "modules": [{"module": module_name, "tree": tree} for module_name, tree in result.modules],
157
+ "resources": result.resources,
158
+ }
159
+ exit_code = 0
160
+ except Exception as e:
161
+ output = _error_output(type(e).__name__, str(e))
162
+ exit_code = 1
163
+
164
+ try:
165
+ data = _encode_output(output)
166
+ except UnicodeEncodeError as e:
167
+ # A file or directory name that is not valid UTF-8, which is allowed on Linux.
168
+ if exit_code == 1:
169
+ # A parser error whose message names such a file. Keep its type and escape the name.
170
+ output = _error_output(output["type"], _escape(output["message"]))
171
+ else:
172
+ # A valid result that contains such a name. Report the output line that contains it.
173
+ line_start = e.object.rfind("\n", 0, e.start) + 1
174
+ line_end = e.object.find("\n", e.end)
175
+ line = e.object[line_start : line_end if line_end != -1 else None].strip().rstrip(",")
176
+ output = _error_output(
177
+ type(e).__name__, f"The parse result holds text that is not valid UTF-8: {_escape(line)}"
178
+ )
179
+ data = _encode_output(output)
180
+ exit_code = 1
181
+ except Exception as e:
182
+ # A value in the result that JSON cannot represent. Report it as a JSON error.
183
+ data = _encode_output(_error_output(type(e).__name__, _escape(str(e))))
184
+ exit_code = 1
185
+
186
+ # Write the UTF-8 bytes to stdout's byte stream, because sys.stdout encodes text with the system encoding, which
187
+ # may not be UTF-8.
188
+ buffer = getattr(sys.stdout, "buffer", None)
189
+ if buffer is None:
190
+ # A Python caller replaced stdout with a stream that takes only text.
191
+ sys.stdout.write(data.decode("utf-8"))
192
+ return exit_code
193
+
194
+ try:
195
+ sys.stdout.flush() # Send out anything already written to sys.stdout first.
196
+ buffer.write(data)
197
+ buffer.flush()
198
+ except BrokenPipeError:
199
+ # The reader closed the pipe early. Send stdout to /dev/null, so Python's flush at exit does not fail again,
200
+ # and keep the exit code.
201
+ devnull = os.open(os.devnull, os.O_WRONLY)
202
+ os.dup2(devnull, sys.stdout.fileno())
203
+ return exit_code
204
+
91
205
 
92
206
  def main(argv: list[str] | None = None) -> int:
93
207
  parser = build_parser()
@@ -96,6 +210,8 @@ def main(argv: list[str] | None = None) -> int:
96
210
  try:
97
211
  # The config file is resolved even when --template-dir is given, so an ambiguous config is always an error.
98
212
  config_template_dir = _template_dir_from_config(parser, args)
213
+ if args.command == "parse":
214
+ return parse(args.plain_file, args.template_dir or config_template_dir)
99
215
  check(args.plain_file, args.template_dir or config_template_dir)
100
216
  except KeyboardInterrupt:
101
217
  return 130
plain_parser/loaders.py CHANGED
@@ -36,20 +36,41 @@ def open_from(dirs, file_name):
36
36
  return None
37
37
 
38
38
 
39
- def resolve_linked_resource(dirs, file_name):
40
- """Return the first existing path for file_name across dirs, or None if not found.
39
+ def find_resource_dir(dirs, file_name):
40
+ """Return the first dir in which file_name exists, or None if not found.
41
41
 
42
42
  Mirrors the search order used by open_from so that validating a linked resource and
43
43
  loading it can never disagree.
44
44
  """
45
45
  for dir in dirs:
46
- full_file_name = os.path.join(dir, file_name)
47
- if os.path.exists(full_file_name):
48
- return full_file_name
46
+ if os.path.exists(os.path.join(dir, file_name)):
47
+ return dir
49
48
 
50
49
  return None
51
50
 
52
51
 
52
+ def resolve_linked_resource(dirs, file_name):
53
+ """Return the first existing path for file_name across dirs, or None if not found."""
54
+ resource_dir = find_resource_dir(dirs, file_name)
55
+ return None if resource_dir is None else os.path.join(resource_dir, file_name)
56
+
57
+
58
+ def relative_resource_path(base_dir: str, resource_dir: str, target: str) -> str:
59
+ """The path of target in resource_dir, relative to base_dir, with `/` as separator.
60
+
61
+ The dir part is computed lexically (symlinks not resolved) and the target is appended as written (`..` kept),
62
+ so joining the result onto base_dir names the same file as os.path.join(resource_dir, target). When no relative
63
+ path exists (another drive on Windows), the absolute path is returned instead, still with `/`.
64
+ """
65
+ try:
66
+ rel_dir = os.path.relpath(resource_dir, base_dir)
67
+ except ValueError:
68
+ return os.path.join(resource_dir, target).replace(os.sep, "/")
69
+ if rel_dir == os.curdir:
70
+ return target
71
+ return rel_dir.replace(os.sep, "/") + "/" + target
72
+
73
+
53
74
  def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
54
75
  linked_resources = {}
55
76
 
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: plain-parser
3
+ Version: 0.1.4.dev10
4
+ Summary: Parser for ***plain specification files
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Software Development :: Compilers
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: mistletoe<2,>=1.6.0
13
+ Requires-Dist: python-frontmatter<2,>=1.3.0
14
+ Requires-Dist: python-liquid2<0.5,>=0.4.0
15
+ Requires-Dist: pyyaml<7,>=6
16
+ Provides-Extra: dev
17
+ Requires-Dist: black==26.5.1; extra == 'dev'
18
+ Requires-Dist: flake8==7.3.0; extra == 'dev'
19
+ Requires-Dist: isort==8.0.1; extra == 'dev'
20
+ Requires-Dist: mypy==2.3.0; extra == 'dev'
21
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # plain-parser
25
+
26
+ Python parser for [`***plain`](https://www.plainlang.org/docs/) specification files.
27
+
28
+ Reads a `.plain` module and its `import` / `requires` chain, resolves Liquid templates,
29
+ validates concepts and linked resources, and returns the marshalled specification tree
30
+ plus the ordered list of functionalities (FRIDs).
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install plain-parser
36
+ ```
37
+
38
+ ## Command line
39
+
40
+ ```
41
+ plain-parser check <file.plain> [--template-dir DIR] [--config-name NAME]
42
+ plain-parser parse <file.plain> [--template-dir DIR] [--config-name NAME]
43
+ ```
44
+
45
+ Both commands parse the module and its `requires` chain, check that every linked resource is an existing
46
+ file (not a directory), and walk the module's functionalities. They stop at the first error.
47
+
48
+ ### `check`
49
+
50
+ Validates the module. In addition to the steps above it reads every linked file and rejects binary files and
51
+ files holding a large base64 blob.
52
+
53
+ ```
54
+ $ plain-parser check my_spec.plain
55
+ my_spec.plain: OK
56
+ ```
57
+
58
+ Exit `0` when the module is valid. On an error, `Error: …` is written to stderr and the exit code is `1`.
59
+
60
+ ### `parse`
61
+
62
+ Prints the parse result as one UTF-8 JSON document on stdout. Linked files are located but not read.
63
+
64
+ ```
65
+ $ plain-parser parse my_spec.plain
66
+ {
67
+ "format_version": 1,
68
+ "modules": [{"module": "...", "tree": {...}}, ...],
69
+ "resources": {...}
70
+ }
71
+ ```
72
+
73
+ - `format_version`: `1`.
74
+ - `modules`: every module in the `requires` chain, in build order: deepest ancestor first, the module itself last.
75
+ Each entry has the module name and its specification tree, in the shape described under *Python* below.
76
+ - `resources`: every linked-resource target in the chain, as written in the spec, mapped to the path it resolves
77
+ to, relative to the entry module's directory, with `/` as separator: `notes.md` when found next to the module,
78
+ `../tpl/notes.md` when found in a template directory. The directory part is lexical (symlinks not resolved) and
79
+ the target is appended as written (`..` kept), so joining the value onto the entry module's directory names the
80
+ file the parser matched. When no relative path exists (another drive on Windows) the value is the absolute path,
81
+ still with `/`. Two spellings of one file (`notes.md`, `./notes.md`) are two keys.
82
+
83
+ Exit `0` on success. On a parser error the JSON is `{"format_version": 1, "type": "<error class>",
84
+ "message": "…"}` and the exit code is `1`.
85
+
86
+ ### Options and exit codes
87
+
88
+ Modules and `{% include %}` templates are looked up in the `.plain` file's directory, then in `--template-dir`.
89
+ When `--template-dir` is not given, it is read from the `template-dir` key of `config.yaml` (or the file named by
90
+ `--config-name`), found next to the `.plain` file or in the working directory. Finding it in both is a usage
91
+ error. The config file is read even when `--template-dir` is given, so a malformed one fails the run either way.
92
+ Other config keys are ignored.
93
+
94
+ | Exit code | Meaning |
95
+ |---|---|
96
+ | `0` | Success |
97
+ | `1` | The module, its chain, or a linked resource is invalid |
98
+ | `2` | Usage error, including an ambiguous or malformed config file |
99
+ | `130` | Interrupted |
100
+
101
+ ## Python
102
+
103
+ ```python
104
+ from plain_parser import plain_file_parser
105
+
106
+ module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
107
+ ```
108
+
109
+ `plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
110
+ `test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
111
+ entries. `plain_parser.plain_spec` has helpers for walking functionalities
112
+ (`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ uv venv --python 3.11 && source .venv/bin/activate
118
+ uv pip install -e ".[dev]"
119
+ pytest
120
+ black . --check && isort . --check-only && flake8 . && mypy src
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,14 @@
1
+ plain_parser/__init__.py,sha256=RzRI_EVcrPnPT_BzoYtrw09-HUzKGrW4HT-GTd8GrTU,725
2
+ plain_parser/cli.py,sha256=0eTf42tuI_i8PVQoA-SbZCZTBK_VL5xBougxuacG4d8,10044
3
+ plain_parser/concept_utils.py,sha256=8NZf5OAKVfz_VvTd1Dn7yiGLv-VFVMjto9YW49xKFjI,9916
4
+ plain_parser/exceptions.py,sha256=WcHK5g9K6bsE-ZZBKWl0lZtiMTQ6fif8-zdEx9qjmRw,795
5
+ plain_parser/graph.py,sha256=xZfgzAXoL_hMfqre0Vj4kdvCC8vzfxZgvYptEK9W0Ho,6843
6
+ plain_parser/liquid_nodes.py,sha256=CYmujdPJMK9hLjfFPmnSoKL3Ps4sb-2OOhH-fZUssis,3917
7
+ plain_parser/loaders.py,sha256=uGV_fTmiYyAtX_dynG1lCfFI9SeM9jS5LBcSENuCO_w,6121
8
+ plain_parser/plain_file.py,sha256=NlcuSbVHS4dbwWEOETv5xY9-mg5y7yZEAvHI6P5maL8,42064
9
+ plain_parser/plain_spec.py,sha256=Z79Vi8bdRiOiM53QyWS8OLNSlGPrQObSZx11Eh8i8Mk,15308
10
+ plain_parser-0.1.4.dev10.dist-info/METADATA,sha256=JXOlzUPyhj8ddxnBH6N3G7fvb-UvZZejhclBQ17Gqrk,4629
11
+ plain_parser-0.1.4.dev10.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
+ plain_parser-0.1.4.dev10.dist-info/entry_points.txt,sha256=pA31GGUyYrmZSpH1CuSzhjrv8pX5ac5HI5hFop1B4eI,55
13
+ plain_parser-0.1.4.dev10.dist-info/licenses/LICENSE,sha256=QClWKncxgRjNUVOW8Axi7zK9HtsMdJEtUSMA4LEe8fo,1071
14
+ plain_parser-0.1.4.dev10.dist-info/RECORD,,
@@ -1,76 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: plain-parser
3
- Version: 0.1.4.dev9
4
- Summary: Parser for ***plain specification files
5
- License-Expression: MIT
6
- License-File: LICENSE
7
- Classifier: Intended Audience :: Developers
8
- Classifier: Operating System :: OS Independent
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Topic :: Software Development :: Compilers
11
- Requires-Python: >=3.11
12
- Requires-Dist: mistletoe<2,>=1.6.0
13
- Requires-Dist: python-frontmatter<2,>=1.3.0
14
- Requires-Dist: python-liquid2<0.5,>=0.4.0
15
- Requires-Dist: pyyaml<7,>=6
16
- Provides-Extra: dev
17
- Requires-Dist: black==26.5.1; extra == 'dev'
18
- Requires-Dist: flake8==7.3.0; extra == 'dev'
19
- Requires-Dist: isort==8.0.1; extra == 'dev'
20
- Requires-Dist: mypy==2.3.0; extra == 'dev'
21
- Requires-Dist: pytest==9.1.1; extra == 'dev'
22
- Description-Content-Type: text/markdown
23
-
24
- # plain-parser
25
-
26
- Python parser for [`***plain`](https://www.plainlang.org/docs/) specification files.
27
-
28
- Reads a `.plain` module and its `import` / `requires` chain, resolves Liquid templates,
29
- validates concepts and linked resources, and returns the marshalled specification tree
30
- plus the ordered list of functionalities (FRIDs).
31
-
32
- ## Install
33
-
34
- ```bash
35
- pip install plain-parser
36
- ```
37
-
38
- ## Usage
39
-
40
- Validate a module and its `requires` chain from the command line (`<file>: OK` and exit `0` when valid; `Error: …` on stderr and exit `1` otherwise):
41
-
42
- ```bash
43
- plain-parser check my_spec.plain [--template-dir DIR] [--config-name NAME]
44
- ```
45
-
46
- Modules and `{% include %}` templates are looked up in the `.plain` file's directory, then in `--template-dir`.
47
- When `--template-dir` is not given, it is read from the `template-dir` key of `config.yaml` (or the file named by
48
- `--config-name`), found next to the `.plain` file or in the working directory. Finding it in both is a usage error
49
- (exit `2`). The config file is read even when `--template-dir` is given, so a malformed one fails the run either way.
50
- Other config keys are ignored.
51
-
52
- Or from Python:
53
-
54
- ```python
55
- from plain_parser import plain_file_parser
56
-
57
- module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
58
- ```
59
-
60
- `plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
61
- `test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
62
- entries. `plain_parser.plain_spec` has helpers for walking functionalities
63
- (`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
64
-
65
- ## Development
66
-
67
- ```bash
68
- uv venv --python 3.11 && source .venv/bin/activate
69
- uv pip install -e ".[dev]"
70
- pytest
71
- black . --check && isort . --check-only && flake8 . && mypy src
72
- ```
73
-
74
- ## License
75
-
76
- MIT, see [LICENSE](LICENSE).
@@ -1,14 +0,0 @@
1
- plain_parser/__init__.py,sha256=RzRI_EVcrPnPT_BzoYtrw09-HUzKGrW4HT-GTd8GrTU,725
2
- plain_parser/cli.py,sha256=oJL_23l_K3ZWpJLRROGjFo2UOsJv7uUoP0RbfL8oit4,4682
3
- plain_parser/concept_utils.py,sha256=8NZf5OAKVfz_VvTd1Dn7yiGLv-VFVMjto9YW49xKFjI,9916
4
- plain_parser/exceptions.py,sha256=WcHK5g9K6bsE-ZZBKWl0lZtiMTQ6fif8-zdEx9qjmRw,795
5
- plain_parser/graph.py,sha256=xZfgzAXoL_hMfqre0Vj4kdvCC8vzfxZgvYptEK9W0Ho,6843
6
- plain_parser/liquid_nodes.py,sha256=CYmujdPJMK9hLjfFPmnSoKL3Ps4sb-2OOhH-fZUssis,3917
7
- plain_parser/loaders.py,sha256=o-I7u1NL8hZdTWztqwO7zw072aGxXTBBYSAFDBRG4q0,5136
8
- plain_parser/plain_file.py,sha256=NlcuSbVHS4dbwWEOETv5xY9-mg5y7yZEAvHI6P5maL8,42064
9
- plain_parser/plain_spec.py,sha256=Z79Vi8bdRiOiM53QyWS8OLNSlGPrQObSZx11Eh8i8Mk,15308
10
- plain_parser-0.1.4.dev9.dist-info/METADATA,sha256=zTBbXnaNNkU7xZwFrONlUU9iOCOT0a9gnXGOBBIkzRQ,2647
11
- plain_parser-0.1.4.dev9.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
- plain_parser-0.1.4.dev9.dist-info/entry_points.txt,sha256=pA31GGUyYrmZSpH1CuSzhjrv8pX5ac5HI5hFop1B4eI,55
13
- plain_parser-0.1.4.dev9.dist-info/licenses/LICENSE,sha256=QClWKncxgRjNUVOW8Axi7zK9HtsMdJEtUSMA4LEe8fo,1071
14
- plain_parser-0.1.4.dev9.dist-info/RECORD,,