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.
- pyegp_parser-0.1.0/CHANGELOG.md +25 -0
- pyegp_parser-0.1.0/CONTRIBUTING.md +64 -0
- pyegp_parser-0.1.0/LICENSE +21 -0
- pyegp_parser-0.1.0/MANIFEST.in +3 -0
- pyegp_parser-0.1.0/PKG-INFO +254 -0
- pyegp_parser-0.1.0/README.md +223 -0
- pyegp_parser-0.1.0/SECURITY.md +59 -0
- pyegp_parser-0.1.0/pyegp_parser/__init__.py +356 -0
- pyegp_parser-0.1.0/pyegp_parser/archive.py +114 -0
- pyegp_parser-0.1.0/pyegp_parser/bulk.py +113 -0
- pyegp_parser-0.1.0/pyegp_parser/classifier.py +60 -0
- pyegp_parser-0.1.0/pyegp_parser/cli.py +195 -0
- pyegp_parser-0.1.0/pyegp_parser/dag.py +100 -0
- pyegp_parser-0.1.0/pyegp_parser/mcp_entry.py +31 -0
- pyegp_parser-0.1.0/pyegp_parser/mcp_server.py +457 -0
- pyegp_parser-0.1.0/pyegp_parser/models/__init__.py +105 -0
- pyegp_parser-0.1.0/pyegp_parser/models/base.py +38 -0
- pyegp_parser-0.1.0/pyegp_parser/models/bulk.py +69 -0
- pyegp_parser-0.1.0/pyegp_parser/models/data.py +59 -0
- pyegp_parser-0.1.0/pyegp_parser/models/elements.py +48 -0
- pyegp_parser-0.1.0/pyegp_parser/models/external_file.py +35 -0
- pyegp_parser-0.1.0/pyegp_parser/models/external_objects.py +32 -0
- pyegp_parser-0.1.0/pyegp_parser/models/log_code.py +43 -0
- pyegp_parser-0.1.0/pyegp_parser/models/process_flow.py +44 -0
- pyegp_parser-0.1.0/pyegp_parser/models/project.py +244 -0
- pyegp_parser-0.1.0/pyegp_parser/models/query.py +160 -0
- pyegp_parser-0.1.0/pyegp_parser/models/shortcut.py +48 -0
- pyegp_parser-0.1.0/pyegp_parser/models/tasks.py +89 -0
- pyegp_parser-0.1.0/pyegp_parser/models/visual_layout.py +111 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/__init__.py +15 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/data_parser.py +369 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/dna_parser.py +182 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/element_parser.py +198 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/external_objects_parser.py +119 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/layout_parser.py +208 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/log_code_parser.py +347 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/ods_parser.py +206 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/pfd_parser.py +152 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/project_parser.py +273 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/query_parser.py +510 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/shortcut_parser.py +83 -0
- pyegp_parser-0.1.0/pyegp_parser/parsers/task_parser.py +460 -0
- pyegp_parser-0.1.0/pyegp_parser/pretty_printer.py +385 -0
- pyegp_parser-0.1.0/pyegp_parser/py.typed +0 -0
- pyegp_parser-0.1.0/pyegp_parser/redaction.py +137 -0
- pyegp_parser-0.1.0/pyegp_parser/schema_generator.py +355 -0
- pyegp_parser-0.1.0/pyegp_parser/serializer.py +342 -0
- pyegp_parser-0.1.0/pyegp_parser/validator.py +56 -0
- pyegp_parser-0.1.0/pyegp_parser.egg-info/PKG-INFO +254 -0
- pyegp_parser-0.1.0/pyegp_parser.egg-info/SOURCES.txt +99 -0
- pyegp_parser-0.1.0/pyegp_parser.egg-info/dependency_links.txt +1 -0
- pyegp_parser-0.1.0/pyegp_parser.egg-info/entry_points.txt +3 -0
- pyegp_parser-0.1.0/pyegp_parser.egg-info/requires.txt +4 -0
- pyegp_parser-0.1.0/pyegp_parser.egg-info/top_level.txt +1 -0
- pyegp_parser-0.1.0/pyproject.toml +132 -0
- pyegp_parser-0.1.0/setup.cfg +4 -0
- pyegp_parser-0.1.0/tests/test_archive.py +255 -0
- pyegp_parser-0.1.0/tests/test_bug_condition_exploration.py +530 -0
- pyegp_parser-0.1.0/tests/test_bulk.py +233 -0
- pyegp_parser-0.1.0/tests/test_cli.py +204 -0
- pyegp_parser-0.1.0/tests/test_dag.py +401 -0
- pyegp_parser-0.1.0/tests/test_data_parser.py +348 -0
- pyegp_parser-0.1.0/tests/test_dna_parser.py +272 -0
- pyegp_parser-0.1.0/tests/test_element_parser.py +372 -0
- pyegp_parser-0.1.0/tests/test_execution_project_log.py +335 -0
- pyegp_parser-0.1.0/tests/test_external_file_parser.py +344 -0
- pyegp_parser-0.1.0/tests/test_external_objects_parser.py +271 -0
- pyegp_parser-0.1.0/tests/test_integration.py +381 -0
- pyegp_parser-0.1.0/tests/test_layout_parser.py +382 -0
- pyegp_parser-0.1.0/tests/test_log_code_parser.py +292 -0
- pyegp_parser-0.1.0/tests/test_mcp_entry.py +60 -0
- pyegp_parser-0.1.0/tests/test_mcp_server.py +138 -0
- pyegp_parser-0.1.0/tests/test_models_base.py +97 -0
- pyegp_parser-0.1.0/tests/test_models_elements.py +445 -0
- pyegp_parser-0.1.0/tests/test_models_project.py +280 -0
- pyegp_parser-0.1.0/tests/test_ods_parser.py +420 -0
- pyegp_parser-0.1.0/tests/test_pfd_parser.py +534 -0
- pyegp_parser-0.1.0/tests/test_preservation_property.py +927 -0
- pyegp_parser-0.1.0/tests/test_pretty_printer.py +459 -0
- pyegp_parser-0.1.0/tests/test_project_parser.py +332 -0
- pyegp_parser-0.1.0/tests/test_prop_completeness.py +241 -0
- pyegp_parser-0.1.0/tests/test_prop_parse_determinism.py +623 -0
- pyegp_parser-0.1.0/tests/test_prop_reference_integrity.py +415 -0
- pyegp_parser-0.1.0/tests/test_property_bulk.py +188 -0
- pyegp_parser-0.1.0/tests/test_property_classification.py +217 -0
- pyegp_parser-0.1.0/tests/test_property_completeness.py +244 -0
- pyegp_parser-0.1.0/tests/test_property_dag.py +322 -0
- pyegp_parser-0.1.0/tests/test_property_dna.py +278 -0
- pyegp_parser-0.1.0/tests/test_property_elements.py +418 -0
- pyegp_parser-0.1.0/tests/test_property_metadata.py +333 -0
- pyegp_parser-0.1.0/tests/test_property_query.py +871 -0
- pyegp_parser-0.1.0/tests/test_property_schema.py +462 -0
- pyegp_parser-0.1.0/tests/test_property_serialization.py +549 -0
- pyegp_parser-0.1.0/tests/test_query_parser.py +709 -0
- pyegp_parser-0.1.0/tests/test_real_world.py +239 -0
- pyegp_parser-0.1.0/tests/test_redaction.py +142 -0
- pyegp_parser-0.1.0/tests/test_schema_generator.py +259 -0
- pyegp_parser-0.1.0/tests/test_serializer.py +598 -0
- pyegp_parser-0.1.0/tests/test_shortcut_parser.py +307 -0
- pyegp_parser-0.1.0/tests/test_task_parser.py +909 -0
- 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,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
|
+
[](https://github.com/lamiskin/pyegp-parser/actions/workflows/ci.yml)
|
|
37
|
+
[](https://codecov.io/gh/lamiskin/pyegp-parser)
|
|
38
|
+
[](https://pypi.org/project/pyegp-parser/)
|
|
39
|
+
[](https://pypi.org/project/pyegp-parser/)
|
|
40
|
+
[](https://github.com/lamiskin/pyegp-parser/actions/workflows/codeql.yml)
|
|
41
|
+
[](https://lamiskin.github.io/pyegp-parser/)
|
|
42
|
+
[](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
|
+
[](https://github.com/lamiskin/pyegp-parser/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/lamiskin/pyegp-parser)
|
|
7
|
+
[](https://pypi.org/project/pyegp-parser/)
|
|
8
|
+
[](https://pypi.org/project/pyegp-parser/)
|
|
9
|
+
[](https://github.com/lamiskin/pyegp-parser/actions/workflows/codeql.yml)
|
|
10
|
+
[](https://lamiskin.github.io/pyegp-parser/)
|
|
11
|
+
[](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.
|