pyegp-parser 0.1.0__tar.gz

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 (101) hide show
  1. pyegp_parser-0.1.0/CHANGELOG.md +25 -0
  2. pyegp_parser-0.1.0/CONTRIBUTING.md +64 -0
  3. pyegp_parser-0.1.0/LICENSE +21 -0
  4. pyegp_parser-0.1.0/MANIFEST.in +3 -0
  5. pyegp_parser-0.1.0/PKG-INFO +254 -0
  6. pyegp_parser-0.1.0/README.md +223 -0
  7. pyegp_parser-0.1.0/SECURITY.md +59 -0
  8. pyegp_parser-0.1.0/pyegp_parser/__init__.py +356 -0
  9. pyegp_parser-0.1.0/pyegp_parser/archive.py +114 -0
  10. pyegp_parser-0.1.0/pyegp_parser/bulk.py +113 -0
  11. pyegp_parser-0.1.0/pyegp_parser/classifier.py +60 -0
  12. pyegp_parser-0.1.0/pyegp_parser/cli.py +195 -0
  13. pyegp_parser-0.1.0/pyegp_parser/dag.py +100 -0
  14. pyegp_parser-0.1.0/pyegp_parser/mcp_entry.py +31 -0
  15. pyegp_parser-0.1.0/pyegp_parser/mcp_server.py +457 -0
  16. pyegp_parser-0.1.0/pyegp_parser/models/__init__.py +105 -0
  17. pyegp_parser-0.1.0/pyegp_parser/models/base.py +38 -0
  18. pyegp_parser-0.1.0/pyegp_parser/models/bulk.py +69 -0
  19. pyegp_parser-0.1.0/pyegp_parser/models/data.py +59 -0
  20. pyegp_parser-0.1.0/pyegp_parser/models/elements.py +48 -0
  21. pyegp_parser-0.1.0/pyegp_parser/models/external_file.py +35 -0
  22. pyegp_parser-0.1.0/pyegp_parser/models/external_objects.py +32 -0
  23. pyegp_parser-0.1.0/pyegp_parser/models/log_code.py +43 -0
  24. pyegp_parser-0.1.0/pyegp_parser/models/process_flow.py +44 -0
  25. pyegp_parser-0.1.0/pyegp_parser/models/project.py +244 -0
  26. pyegp_parser-0.1.0/pyegp_parser/models/query.py +160 -0
  27. pyegp_parser-0.1.0/pyegp_parser/models/shortcut.py +48 -0
  28. pyegp_parser-0.1.0/pyegp_parser/models/tasks.py +89 -0
  29. pyegp_parser-0.1.0/pyegp_parser/models/visual_layout.py +111 -0
  30. pyegp_parser-0.1.0/pyegp_parser/parsers/__init__.py +15 -0
  31. pyegp_parser-0.1.0/pyegp_parser/parsers/data_parser.py +369 -0
  32. pyegp_parser-0.1.0/pyegp_parser/parsers/dna_parser.py +182 -0
  33. pyegp_parser-0.1.0/pyegp_parser/parsers/element_parser.py +198 -0
  34. pyegp_parser-0.1.0/pyegp_parser/parsers/external_objects_parser.py +119 -0
  35. pyegp_parser-0.1.0/pyegp_parser/parsers/layout_parser.py +208 -0
  36. pyegp_parser-0.1.0/pyegp_parser/parsers/log_code_parser.py +347 -0
  37. pyegp_parser-0.1.0/pyegp_parser/parsers/ods_parser.py +206 -0
  38. pyegp_parser-0.1.0/pyegp_parser/parsers/pfd_parser.py +152 -0
  39. pyegp_parser-0.1.0/pyegp_parser/parsers/project_parser.py +273 -0
  40. pyegp_parser-0.1.0/pyegp_parser/parsers/query_parser.py +510 -0
  41. pyegp_parser-0.1.0/pyegp_parser/parsers/shortcut_parser.py +83 -0
  42. pyegp_parser-0.1.0/pyegp_parser/parsers/task_parser.py +460 -0
  43. pyegp_parser-0.1.0/pyegp_parser/pretty_printer.py +385 -0
  44. pyegp_parser-0.1.0/pyegp_parser/py.typed +0 -0
  45. pyegp_parser-0.1.0/pyegp_parser/redaction.py +137 -0
  46. pyegp_parser-0.1.0/pyegp_parser/schema_generator.py +355 -0
  47. pyegp_parser-0.1.0/pyegp_parser/serializer.py +342 -0
  48. pyegp_parser-0.1.0/pyegp_parser/validator.py +56 -0
  49. pyegp_parser-0.1.0/pyegp_parser.egg-info/PKG-INFO +254 -0
  50. pyegp_parser-0.1.0/pyegp_parser.egg-info/SOURCES.txt +99 -0
  51. pyegp_parser-0.1.0/pyegp_parser.egg-info/dependency_links.txt +1 -0
  52. pyegp_parser-0.1.0/pyegp_parser.egg-info/entry_points.txt +3 -0
  53. pyegp_parser-0.1.0/pyegp_parser.egg-info/requires.txt +4 -0
  54. pyegp_parser-0.1.0/pyegp_parser.egg-info/top_level.txt +1 -0
  55. pyegp_parser-0.1.0/pyproject.toml +132 -0
  56. pyegp_parser-0.1.0/setup.cfg +4 -0
  57. pyegp_parser-0.1.0/tests/test_archive.py +255 -0
  58. pyegp_parser-0.1.0/tests/test_bug_condition_exploration.py +530 -0
  59. pyegp_parser-0.1.0/tests/test_bulk.py +233 -0
  60. pyegp_parser-0.1.0/tests/test_cli.py +204 -0
  61. pyegp_parser-0.1.0/tests/test_dag.py +401 -0
  62. pyegp_parser-0.1.0/tests/test_data_parser.py +348 -0
  63. pyegp_parser-0.1.0/tests/test_dna_parser.py +272 -0
  64. pyegp_parser-0.1.0/tests/test_element_parser.py +372 -0
  65. pyegp_parser-0.1.0/tests/test_execution_project_log.py +335 -0
  66. pyegp_parser-0.1.0/tests/test_external_file_parser.py +344 -0
  67. pyegp_parser-0.1.0/tests/test_external_objects_parser.py +271 -0
  68. pyegp_parser-0.1.0/tests/test_integration.py +381 -0
  69. pyegp_parser-0.1.0/tests/test_layout_parser.py +382 -0
  70. pyegp_parser-0.1.0/tests/test_log_code_parser.py +292 -0
  71. pyegp_parser-0.1.0/tests/test_mcp_entry.py +60 -0
  72. pyegp_parser-0.1.0/tests/test_mcp_server.py +138 -0
  73. pyegp_parser-0.1.0/tests/test_models_base.py +97 -0
  74. pyegp_parser-0.1.0/tests/test_models_elements.py +445 -0
  75. pyegp_parser-0.1.0/tests/test_models_project.py +280 -0
  76. pyegp_parser-0.1.0/tests/test_ods_parser.py +420 -0
  77. pyegp_parser-0.1.0/tests/test_pfd_parser.py +534 -0
  78. pyegp_parser-0.1.0/tests/test_preservation_property.py +927 -0
  79. pyegp_parser-0.1.0/tests/test_pretty_printer.py +459 -0
  80. pyegp_parser-0.1.0/tests/test_project_parser.py +332 -0
  81. pyegp_parser-0.1.0/tests/test_prop_completeness.py +241 -0
  82. pyegp_parser-0.1.0/tests/test_prop_parse_determinism.py +623 -0
  83. pyegp_parser-0.1.0/tests/test_prop_reference_integrity.py +415 -0
  84. pyegp_parser-0.1.0/tests/test_property_bulk.py +188 -0
  85. pyegp_parser-0.1.0/tests/test_property_classification.py +217 -0
  86. pyegp_parser-0.1.0/tests/test_property_completeness.py +244 -0
  87. pyegp_parser-0.1.0/tests/test_property_dag.py +322 -0
  88. pyegp_parser-0.1.0/tests/test_property_dna.py +278 -0
  89. pyegp_parser-0.1.0/tests/test_property_elements.py +418 -0
  90. pyegp_parser-0.1.0/tests/test_property_metadata.py +333 -0
  91. pyegp_parser-0.1.0/tests/test_property_query.py +871 -0
  92. pyegp_parser-0.1.0/tests/test_property_schema.py +462 -0
  93. pyegp_parser-0.1.0/tests/test_property_serialization.py +549 -0
  94. pyegp_parser-0.1.0/tests/test_query_parser.py +709 -0
  95. pyegp_parser-0.1.0/tests/test_real_world.py +239 -0
  96. pyegp_parser-0.1.0/tests/test_redaction.py +142 -0
  97. pyegp_parser-0.1.0/tests/test_schema_generator.py +259 -0
  98. pyegp_parser-0.1.0/tests/test_serializer.py +598 -0
  99. pyegp_parser-0.1.0/tests/test_shortcut_parser.py +307 -0
  100. pyegp_parser-0.1.0/tests/test_task_parser.py +909 -0
  101. pyegp_parser-0.1.0/tests/test_validator.py +175 -0
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-08-28
10
+
11
+ ### Added
12
+
13
+ - Initial public release.
14
+ - Parse SAS Enterprise Guide `.egp` project files into structured JSON.
15
+ - Extraction of project metadata, elements, tasks, queries, data references, and shortcuts.
16
+ - Execution DAG reconstruction and data-lineage tracing.
17
+ - Embedded SAS code, execution log, and ODS result extraction.
18
+ - CLI (`pyegp-parser`) with `parse`, `bulk`, and `print` commands.
19
+ - Optional MCP server (`pyegp-parser-mcp`) and a Claude Agent Skill for AI-assisted exploration.
20
+ - Optional JSON-schema validation of the output.
21
+ - Best-effort credential redaction for passwords embedded in SAS code,
22
+ execution logs, and task configuration.
23
+
24
+ [Unreleased]: https://github.com/lamiskin/pyegp-parser/compare/v0.1.0...HEAD
25
+ [0.1.0]: https://github.com/lamiskin/pyegp-parser/releases/tag/v0.1.0
@@ -0,0 +1,64 @@
1
+ # Contributing to pyegp-parser
2
+
3
+ Thanks for your interest in contributing!
4
+
5
+ ## Development setup
6
+
7
+ This project uses [uv](https://docs.astral.sh/uv/).
8
+
9
+ ```bash
10
+ git clone https://github.com/lamiskin/pyegp-parser
11
+ cd pyegp-parser
12
+ uv sync --all-extras # create the venv and install everything
13
+ uv run pre-commit install # enable the git hooks (optional but recommended)
14
+ ```
15
+
16
+ ## Everyday commands
17
+
18
+ ```bash
19
+ uv run pytest # run tests
20
+ uv run ruff check # lint
21
+ uv run ruff format # auto-format
22
+ uv run mypy # type-check
23
+ ```
24
+
25
+ All of the above run in CI on every pull request across Python 3.11–3.14.
26
+
27
+ ## Guidelines
28
+
29
+ - **Never commit real or confidential `.egp` data.** The test suite generates all
30
+ input in-memory. Integration tests that need real files skip automatically when
31
+ none are present — keep it that way.
32
+ - Add or update tests for any behaviour change.
33
+ - Keep public APIs typed (the package ships `py.typed`).
34
+ - Do not bump versions or edit `CHANGELOG.md` by hand — release-please owns
35
+ both, and generates them from your commit messages.
36
+
37
+ ## Commit messages
38
+
39
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/)
40
+ (e.g. `feat:`, `fix:`, `docs:`, `ci:`) so releases and the changelog can be
41
+ generated automatically.
42
+
43
+ ## Releases
44
+
45
+ Releases are automated with release-please: merging the generated "release" PR
46
+ tags a new version and publishes to PyPI via Trusted Publishing. Maintainers do
47
+ not publish manually.
48
+
49
+ ### Bootstrapping the first release (maintainers)
50
+
51
+ release-please treats `.release-please-manifest.json` as the last *released*
52
+ version, so it will only ever propose versions **after** it. The version
53
+ currently recorded there has to be tagged and released by hand once, or it never
54
+ reaches PyPI:
55
+
56
+ ```bash
57
+ git tag -a v0.1.0 -m "pyegp-parser 0.1.0" && git push origin v0.1.0
58
+ gh release create v0.1.0 --notes-from-tag
59
+ ```
60
+
61
+ Before that, dry-run the whole pipeline without touching real PyPI: run the
62
+ **Publish** workflow manually (`workflow_dispatch`) with target `testpypi`. It
63
+ builds, validates the metadata, smoke-tests the wheel, and publishes to TestPyPI
64
+ only — the PyPI job is unreachable from that trigger.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lachlan Miskin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include CHANGELOG.md
2
+ include CONTRIBUTING.md
3
+ include SECURITY.md
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyegp-parser
3
+ Version: 0.1.0
4
+ Summary: Parse SAS Enterprise Guide .egp project files into structured JSON
5
+ Author-email: Lachlan Miskin <lamiskin@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lamiskin/pyegp-parser
8
+ Project-URL: Repository, https://github.com/lamiskin/pyegp-parser
9
+ Project-URL: Issues, https://github.com/lamiskin/pyegp-parser/issues
10
+ Project-URL: Changelog, https://github.com/lamiskin/pyegp-parser/blob/main/CHANGELOG.md
11
+ Keywords: sas,enterprise guide,egp,parser,etl,data lineage,project.xml
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Text Processing :: Markup :: XML
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: jsonschema
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=2.0.0; extra == "mcp"
30
+ Dynamic: license-file
31
+
32
+ # pyegp-parser
33
+
34
+ Parse **SAS Enterprise Guide `.egp` project files** into structured, machine-readable JSON.
35
+
36
+ [![CI](https://github.com/lamiskin/pyegp-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/lamiskin/pyegp-parser/actions/workflows/ci.yml)
37
+ [![codecov](https://codecov.io/gh/lamiskin/pyegp-parser/branch/main/graph/badge.svg)](https://codecov.io/gh/lamiskin/pyegp-parser)
38
+ [![PyPI](https://img.shields.io/pypi/v/pyegp-parser.svg)](https://pypi.org/project/pyegp-parser/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyegp-parser.svg)](https://pypi.org/project/pyegp-parser/)
40
+ [![CodeQL](https://github.com/lamiskin/pyegp-parser/actions/workflows/codeql.yml/badge.svg)](https://github.com/lamiskin/pyegp-parser/actions/workflows/codeql.yml)
41
+ [![Docs](https://img.shields.io/badge/docs-lamiskin.github.io-blue)](https://lamiskin.github.io/pyegp-parser/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
+
44
+ An `.egp` file is a ZIP archive containing `project.xml` plus SAS code, task
45
+ configs, execution logs, and ODS results. `pyegp-parser` reads **all** of it and
46
+ produces a single structured representation — think of it as decompiling a SAS
47
+ Enterprise Guide project into JSON you can query, diff, and analyse.
48
+
49
+ ## Features
50
+
51
+ - Full extraction of project metadata, elements, tasks, queries, data references, and shortcuts
52
+ - Reconstructs the execution **DAG** (process-flow dependencies)
53
+ - Extracts embedded SAS code, execution logs, and ODS output references
54
+ - Data-lineage tracing across tasks
55
+ - Best-effort credential redaction for passwords embedded in SAS code
56
+ - Bulk-parse an entire directory tree of `.egp` files
57
+ - Optional JSON-schema validation of the output
58
+ - Optional [MCP](https://modelcontextprotocol.io) server for AI-assisted exploration
59
+ - No heavy dependencies (just `jsonschema`); pure-Python, cross-platform
60
+
61
+ ## Supported Enterprise Guide versions
62
+
63
+ The parser is **version-agnostic by design**: it reads whatever structure a
64
+ project contains and has no version gates or version-specific branches. The
65
+ `EGVersion` attribute on the root `ProjectCollection` element is recorded and
66
+ surfaced as `project.metadata.eg_version`, but it never changes how a file is
67
+ parsed. In practice that means an unlisted version is likely to parse, and
68
+ anything the parser does not recognise is reported rather than dropped — see
69
+ [Handling unknown content](#handling-unknown-content) below.
70
+
71
+ | EG version | Status | How it is verified |
72
+ |---|---|---|
73
+ | **8.1** | Verified against a real project | `tests/fixtures/real_world/eg81_process_flows.egp`, plus most of the synthetic suite |
74
+ | **7.1** | Verified against a real project | `tests/fixtures/real_world/eg71_code_tasks.egp` |
75
+ | Other 7.x / 8.x | Expected to work, untested | No version gating exists, but no sample was available |
76
+ | 9.x and later | Unknown | Not released at the time of writing |
77
+
78
+ Two caveats worth stating plainly:
79
+
80
+ - The 7.1 fixture was produced by a third-party 8→7 downgrade converter rather
81
+ than written by Enterprise Guide 7.1 itself. Its `project.xml`, embedded SAS
82
+ code, and execution logs are EG-authored, but it is not a pristine 7.1 file.
83
+ - The 8.1 fixture is a *migrated* project: its elements carry
84
+ `ModifiedByEGVer` values spanning `7.100.5.x` and `8.1.0.x`, so mixed-version
85
+ element metadata within a single project is covered.
86
+
87
+ Real-world coverage is limited by sample availability — genuine `.egp` files are
88
+ rarely published, since they are binary ZIP archives. Provenance and the exact
89
+ sanitisation applied to both fixtures are documented in
90
+ [`tests/fixtures/real_world/README.md`](tests/fixtures/real_world/README.md).
91
+
92
+ ### Handling unknown content
93
+
94
+ Because there is no version gating, an unlisted version is likely to parse. How
95
+ the parser behaves when it meets something it does not understand depends on
96
+ what that something is:
97
+
98
+ - **Unrecognised archive entries** are listed in `project.unprocessed_entries`
99
+ and counted in `project.completeness_summary`, so nothing is silently
100
+ discarded. Comparing `processed_entries` against `total_entries` tells you how
101
+ much of a project was understood.
102
+ - **Missing optional artifacts** — an absent `code.sas` or task-config file —
103
+ log a warning and leave the corresponding field `None`.
104
+ - **A missing required section inside an element** raises `ValueError` and
105
+ aborts the whole parse. This is deliberate: the parser surfaces structural
106
+ gaps rather than emitting quietly incomplete output. Log elements and
107
+ process-flow containers are the two exceptions — they are recorded with
108
+ whatever metadata was readable, because a display-settings or DAG failure does
109
+ not undermine the rest of the project.
110
+
111
+ So a newer EG version introducing a *new* element type or archive entry is
112
+ handled gracefully, whereas one that *restructures an existing element* will
113
+ fail loudly. If you hit either, `completeness_summary` and `unprocessed_entries`
114
+ are the place to look first, and a bug report quoting the EG version plus those
115
+ fields is the most useful thing you can send.
116
+
117
+ ## Installation
118
+
119
+ ```bash
120
+ pip install pyegp-parser
121
+ ```
122
+
123
+ Requires Python 3.11+.
124
+
125
+ ## Quick start
126
+
127
+ ```python
128
+ from pyegp_parser import parse_file
129
+
130
+ project = parse_file("path/to/project.egp")
131
+
132
+ print(project.metadata.label) # project name
133
+ print(len(project.tasks)) # number of tasks
134
+ print(len(project.queries)) # number of Query Builder queries
135
+ ```
136
+
137
+ Write JSON to disk:
138
+
139
+ ```python
140
+ parse_file("project.egp", output_dir="./output") # writes ./output/project.json
141
+ ```
142
+
143
+ ## CLI
144
+
145
+ ```bash
146
+ # Parse a single file
147
+ pyegp-parser parse path/to/project.egp --output-dir ./output
148
+
149
+ # Recursively parse every .egp under a directory (mirrors the tree)
150
+ pyegp-parser bulk ./source-pipeline --output-dir ./output
151
+
152
+ # Pretty-print a parsed project.json
153
+ pyegp-parser print ./output/project.json
154
+ ```
155
+
156
+ ## Credential redaction
157
+
158
+ `.egp` projects embed SAS programs and logs verbatim, and SAS code routinely
159
+ carries live credentials. Passwords are therefore redacted automatically when
160
+ output is written:
161
+
162
+ ```python
163
+ from pyegp_parser.redaction import redact_text
164
+
165
+ redact_text("libname dw oracle user=etluser password=hunter2 path=prod;")
166
+ # ('libname dw oracle user=etluser password=[SENSITIVE - REDACTED] path=prod;', 1)
167
+ ```
168
+
169
+ SAS-encoded passwords (`{SAS002}...`) are redacted too — `PROC PWENCODE` output
170
+ is reversible, so it is treated as plaintext. Column names such as
171
+ `PASSWORD_HASH` are deliberately left intact: a column *name* is schema
172
+ metadata, not a secret.
173
+
174
+ Redaction applies when `project.json` is written and to MCP server output.
175
+ `to_dict()` is a pure serializer and does not redact.
176
+
177
+ ### Handling real projects
178
+
179
+ Redaction covers credentials, not everything an `.egp` file reveals. Output also
180
+ includes library paths, server names, schema names, and usernames. Review parser
181
+ output before attaching it to a public issue or sharing it outside your
182
+ organisation — see [SECURITY.md](SECURITY.md) for the full security model.
183
+
184
+ ## AI integration
185
+
186
+ `.egp` projects are dense and hard to read by hand, which makes them a natural fit
187
+ for AI-assisted exploration. Two complementary options ship with this project:
188
+
189
+ ### MCP server
190
+
191
+ An [MCP](https://modelcontextprotocol.io) server exposes the parser as tools
192
+ (`parse_egp`, `parse_egp_directory`, `get_project_summary`, `get_sas_code`,
193
+ `get_data_lineage`, `get_queries`) to any MCP-compatible client (Claude Desktop,
194
+ Claude Code, Cursor, …).
195
+
196
+ ```bash
197
+ pip install "pyegp-parser[mcp]"
198
+ ```
199
+
200
+ Then register it with your client. Example config:
201
+
202
+ ```json
203
+ {
204
+ "mcpServers": {
205
+ "pyegp-parser": {
206
+ "command": "pyegp-parser-mcp"
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ ### Claude Skill
213
+
214
+ [`skills/pyegp-parser/SKILL.md`](skills/pyegp-parser/SKILL.md) is a portable
215
+ [Agent Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview)
216
+ that teaches Claude when and how to parse `.egp` files and interpret the JSON —
217
+ no running server required. Copy the `skills/pyegp-parser/` folder into your
218
+ `.claude/skills/` directory to use it.
219
+
220
+ Use the **MCP server** for interactive, on-demand parsing inside a client; use the
221
+ **Skill** for a portable, dependency-light way to give any Claude the know-how.
222
+
223
+ ## Documentation
224
+
225
+ Full documentation lives at
226
+ **[lamiskin.github.io/pyegp-parser](https://lamiskin.github.io/pyegp-parser/)**.
227
+
228
+ - [Full usage guide & API reference](docs/reference.md)
229
+ - [Guide to interpreting the JSON output (for LLMs and humans)](docs/LLM_CONTEXT.md)
230
+
231
+ ## Development
232
+
233
+ This project uses [uv](https://docs.astral.sh/uv/) and [ruff](https://docs.astral.sh/ruff/).
234
+
235
+ ```bash
236
+ git clone https://github.com/lamiskin/pyegp-parser
237
+ cd pyegp-parser
238
+ uv sync # create the venv and install deps (incl. dev tools)
239
+ uv run pytest # run the test suite
240
+ uv run ruff check # lint
241
+ uv run ruff format # format
242
+ ```
243
+
244
+ ## Acknowledgements
245
+
246
+ This library was developed with substantial assistance from AI coding tools. It
247
+ was originally built and validated against a corpus of **real** SAS Enterprise
248
+ Guide `.egp` files during a data-migration project. None of that source data — and
249
+ no files, identifiers, or history derived from it — is included in this repository;
250
+ the test suite runs entirely on synthetic fixtures generated in-memory.
251
+
252
+ ## License
253
+
254
+ [MIT](LICENSE)
@@ -0,0 +1,223 @@
1
+ # pyegp-parser
2
+
3
+ Parse **SAS Enterprise Guide `.egp` project files** into structured, machine-readable JSON.
4
+
5
+ [![CI](https://github.com/lamiskin/pyegp-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/lamiskin/pyegp-parser/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/lamiskin/pyegp-parser/branch/main/graph/badge.svg)](https://codecov.io/gh/lamiskin/pyegp-parser)
7
+ [![PyPI](https://img.shields.io/pypi/v/pyegp-parser.svg)](https://pypi.org/project/pyegp-parser/)
8
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyegp-parser.svg)](https://pypi.org/project/pyegp-parser/)
9
+ [![CodeQL](https://github.com/lamiskin/pyegp-parser/actions/workflows/codeql.yml/badge.svg)](https://github.com/lamiskin/pyegp-parser/actions/workflows/codeql.yml)
10
+ [![Docs](https://img.shields.io/badge/docs-lamiskin.github.io-blue)](https://lamiskin.github.io/pyegp-parser/)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
12
+
13
+ An `.egp` file is a ZIP archive containing `project.xml` plus SAS code, task
14
+ configs, execution logs, and ODS results. `pyegp-parser` reads **all** of it and
15
+ produces a single structured representation — think of it as decompiling a SAS
16
+ Enterprise Guide project into JSON you can query, diff, and analyse.
17
+
18
+ ## Features
19
+
20
+ - Full extraction of project metadata, elements, tasks, queries, data references, and shortcuts
21
+ - Reconstructs the execution **DAG** (process-flow dependencies)
22
+ - Extracts embedded SAS code, execution logs, and ODS output references
23
+ - Data-lineage tracing across tasks
24
+ - Best-effort credential redaction for passwords embedded in SAS code
25
+ - Bulk-parse an entire directory tree of `.egp` files
26
+ - Optional JSON-schema validation of the output
27
+ - Optional [MCP](https://modelcontextprotocol.io) server for AI-assisted exploration
28
+ - No heavy dependencies (just `jsonschema`); pure-Python, cross-platform
29
+
30
+ ## Supported Enterprise Guide versions
31
+
32
+ The parser is **version-agnostic by design**: it reads whatever structure a
33
+ project contains and has no version gates or version-specific branches. The
34
+ `EGVersion` attribute on the root `ProjectCollection` element is recorded and
35
+ surfaced as `project.metadata.eg_version`, but it never changes how a file is
36
+ parsed. In practice that means an unlisted version is likely to parse, and
37
+ anything the parser does not recognise is reported rather than dropped — see
38
+ [Handling unknown content](#handling-unknown-content) below.
39
+
40
+ | EG version | Status | How it is verified |
41
+ |---|---|---|
42
+ | **8.1** | Verified against a real project | `tests/fixtures/real_world/eg81_process_flows.egp`, plus most of the synthetic suite |
43
+ | **7.1** | Verified against a real project | `tests/fixtures/real_world/eg71_code_tasks.egp` |
44
+ | Other 7.x / 8.x | Expected to work, untested | No version gating exists, but no sample was available |
45
+ | 9.x and later | Unknown | Not released at the time of writing |
46
+
47
+ Two caveats worth stating plainly:
48
+
49
+ - The 7.1 fixture was produced by a third-party 8→7 downgrade converter rather
50
+ than written by Enterprise Guide 7.1 itself. Its `project.xml`, embedded SAS
51
+ code, and execution logs are EG-authored, but it is not a pristine 7.1 file.
52
+ - The 8.1 fixture is a *migrated* project: its elements carry
53
+ `ModifiedByEGVer` values spanning `7.100.5.x` and `8.1.0.x`, so mixed-version
54
+ element metadata within a single project is covered.
55
+
56
+ Real-world coverage is limited by sample availability — genuine `.egp` files are
57
+ rarely published, since they are binary ZIP archives. Provenance and the exact
58
+ sanitisation applied to both fixtures are documented in
59
+ [`tests/fixtures/real_world/README.md`](tests/fixtures/real_world/README.md).
60
+
61
+ ### Handling unknown content
62
+
63
+ Because there is no version gating, an unlisted version is likely to parse. How
64
+ the parser behaves when it meets something it does not understand depends on
65
+ what that something is:
66
+
67
+ - **Unrecognised archive entries** are listed in `project.unprocessed_entries`
68
+ and counted in `project.completeness_summary`, so nothing is silently
69
+ discarded. Comparing `processed_entries` against `total_entries` tells you how
70
+ much of a project was understood.
71
+ - **Missing optional artifacts** — an absent `code.sas` or task-config file —
72
+ log a warning and leave the corresponding field `None`.
73
+ - **A missing required section inside an element** raises `ValueError` and
74
+ aborts the whole parse. This is deliberate: the parser surfaces structural
75
+ gaps rather than emitting quietly incomplete output. Log elements and
76
+ process-flow containers are the two exceptions — they are recorded with
77
+ whatever metadata was readable, because a display-settings or DAG failure does
78
+ not undermine the rest of the project.
79
+
80
+ So a newer EG version introducing a *new* element type or archive entry is
81
+ handled gracefully, whereas one that *restructures an existing element* will
82
+ fail loudly. If you hit either, `completeness_summary` and `unprocessed_entries`
83
+ are the place to look first, and a bug report quoting the EG version plus those
84
+ fields is the most useful thing you can send.
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install pyegp-parser
90
+ ```
91
+
92
+ Requires Python 3.11+.
93
+
94
+ ## Quick start
95
+
96
+ ```python
97
+ from pyegp_parser import parse_file
98
+
99
+ project = parse_file("path/to/project.egp")
100
+
101
+ print(project.metadata.label) # project name
102
+ print(len(project.tasks)) # number of tasks
103
+ print(len(project.queries)) # number of Query Builder queries
104
+ ```
105
+
106
+ Write JSON to disk:
107
+
108
+ ```python
109
+ parse_file("project.egp", output_dir="./output") # writes ./output/project.json
110
+ ```
111
+
112
+ ## CLI
113
+
114
+ ```bash
115
+ # Parse a single file
116
+ pyegp-parser parse path/to/project.egp --output-dir ./output
117
+
118
+ # Recursively parse every .egp under a directory (mirrors the tree)
119
+ pyegp-parser bulk ./source-pipeline --output-dir ./output
120
+
121
+ # Pretty-print a parsed project.json
122
+ pyegp-parser print ./output/project.json
123
+ ```
124
+
125
+ ## Credential redaction
126
+
127
+ `.egp` projects embed SAS programs and logs verbatim, and SAS code routinely
128
+ carries live credentials. Passwords are therefore redacted automatically when
129
+ output is written:
130
+
131
+ ```python
132
+ from pyegp_parser.redaction import redact_text
133
+
134
+ redact_text("libname dw oracle user=etluser password=hunter2 path=prod;")
135
+ # ('libname dw oracle user=etluser password=[SENSITIVE - REDACTED] path=prod;', 1)
136
+ ```
137
+
138
+ SAS-encoded passwords (`{SAS002}...`) are redacted too — `PROC PWENCODE` output
139
+ is reversible, so it is treated as plaintext. Column names such as
140
+ `PASSWORD_HASH` are deliberately left intact: a column *name* is schema
141
+ metadata, not a secret.
142
+
143
+ Redaction applies when `project.json` is written and to MCP server output.
144
+ `to_dict()` is a pure serializer and does not redact.
145
+
146
+ ### Handling real projects
147
+
148
+ Redaction covers credentials, not everything an `.egp` file reveals. Output also
149
+ includes library paths, server names, schema names, and usernames. Review parser
150
+ output before attaching it to a public issue or sharing it outside your
151
+ organisation — see [SECURITY.md](SECURITY.md) for the full security model.
152
+
153
+ ## AI integration
154
+
155
+ `.egp` projects are dense and hard to read by hand, which makes them a natural fit
156
+ for AI-assisted exploration. Two complementary options ship with this project:
157
+
158
+ ### MCP server
159
+
160
+ An [MCP](https://modelcontextprotocol.io) server exposes the parser as tools
161
+ (`parse_egp`, `parse_egp_directory`, `get_project_summary`, `get_sas_code`,
162
+ `get_data_lineage`, `get_queries`) to any MCP-compatible client (Claude Desktop,
163
+ Claude Code, Cursor, …).
164
+
165
+ ```bash
166
+ pip install "pyegp-parser[mcp]"
167
+ ```
168
+
169
+ Then register it with your client. Example config:
170
+
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "pyegp-parser": {
175
+ "command": "pyegp-parser-mcp"
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### Claude Skill
182
+
183
+ [`skills/pyegp-parser/SKILL.md`](skills/pyegp-parser/SKILL.md) is a portable
184
+ [Agent Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview)
185
+ that teaches Claude when and how to parse `.egp` files and interpret the JSON —
186
+ no running server required. Copy the `skills/pyegp-parser/` folder into your
187
+ `.claude/skills/` directory to use it.
188
+
189
+ Use the **MCP server** for interactive, on-demand parsing inside a client; use the
190
+ **Skill** for a portable, dependency-light way to give any Claude the know-how.
191
+
192
+ ## Documentation
193
+
194
+ Full documentation lives at
195
+ **[lamiskin.github.io/pyegp-parser](https://lamiskin.github.io/pyegp-parser/)**.
196
+
197
+ - [Full usage guide & API reference](docs/reference.md)
198
+ - [Guide to interpreting the JSON output (for LLMs and humans)](docs/LLM_CONTEXT.md)
199
+
200
+ ## Development
201
+
202
+ This project uses [uv](https://docs.astral.sh/uv/) and [ruff](https://docs.astral.sh/ruff/).
203
+
204
+ ```bash
205
+ git clone https://github.com/lamiskin/pyegp-parser
206
+ cd pyegp-parser
207
+ uv sync # create the venv and install deps (incl. dev tools)
208
+ uv run pytest # run the test suite
209
+ uv run ruff check # lint
210
+ uv run ruff format # format
211
+ ```
212
+
213
+ ## Acknowledgements
214
+
215
+ This library was developed with substantial assistance from AI coding tools. It
216
+ was originally built and validated against a corpus of **real** SAS Enterprise
217
+ Guide `.egp` files during a data-migration project. None of that source data — and
218
+ no files, identifiers, or history derived from it — is included in this repository;
219
+ the test suite runs entirely on synthetic fixtures generated in-memory.
220
+
221
+ ## License
222
+
223
+ [MIT](LICENSE)
@@ -0,0 +1,59 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest released version on PyPI receives security fixes.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report security issues privately via GitHub's
10
+ [private vulnerability reporting](https://github.com/lamiskin/pyegp-parser/security/advisories/new)
11
+ rather than opening a public issue.
12
+
13
+ Please do **not** include real or confidential `.egp` data or parsed output in
14
+ your report — a minimal synthetic reproduction is preferred.
15
+
16
+ You can expect an initial response within a reasonable timeframe. Thanks for
17
+ helping keep the project and its users safe.
18
+
19
+ ## Security model, and what this tool does not guarantee
20
+
21
+ `pyegp-parser` reads ZIP archives and XML and produces JSON. It executes no SAS
22
+ code, opens no database connections, and resolves no external XML entities.
23
+
24
+ The important thing to understand about `.egp` files is that they carry
25
+ **embedded SAS programs and execution logs**, reproduced verbatim in parser
26
+ output. SAS code routinely contains live credentials:
27
+
28
+ ```sas
29
+ libname dw oracle user=etluser password=hunter2 path=prod;
30
+ ```
31
+
32
+ So the parser performs **best-effort credential redaction** when it writes
33
+ output. Detected credentials are replaced with `[SENSITIVE - REDACTED]`:
34
+
35
+ - `password=` / `passwd=` / `pwd=` / `pw=` / `pass=` assignments, quoted or bare
36
+ - SAS-encoded passwords (`{SAS002}...`) — `PROC PWENCODE` output is reversible,
37
+ so it is treated as plaintext
38
+ - `%LET` macro assignments whose variable name looks password-like
39
+ - Values under a password-like key
40
+
41
+ Redaction is applied when a `project.json` is written and to output returned by
42
+ the MCP server. `to_dict()` is a pure serializer and does **not** redact — call
43
+ `pyegp_parser.redaction.redact()` yourself if you route output elsewhere, or
44
+ pass `redact=False` to `serialize_project()` if you deliberately need
45
+ byte-faithful code.
46
+
47
+ Treat that as a convenience, not a security boundary:
48
+
49
+ - Redaction targets credentials. It does **not** remove server names, library
50
+ paths, database or schema names, file paths, or usernames — all of which SAS
51
+ code and EG logs contain freely.
52
+ - A credential in an unusual form (a custom macro, a value assembled at runtime,
53
+ an authentication-domain reference) will not match the patterns.
54
+ - Column and macro-variable *names* containing "password" are deliberately left
55
+ intact, because they are schema metadata rather than secrets.
56
+
57
+ **Review parser output before sharing it** outside your organisation, attaching
58
+ it to an issue, or feeding it to an external service. If you find a credential
59
+ that survives redaction, please report it privately as a vulnerability.