plain-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 (115) hide show
  1. plain_parser-0.1.0/.flake8 +13 -0
  2. plain_parser-0.1.0/.github/workflows/lint-and-test.yml +44 -0
  3. plain_parser-0.1.0/.github/workflows/publish-dev-to-pypi.yml +147 -0
  4. plain_parser-0.1.0/.github/workflows/publish-to-pypi.yml +117 -0
  5. plain_parser-0.1.0/.gitignore +8 -0
  6. plain_parser-0.1.0/LICENSE +21 -0
  7. plain_parser-0.1.0/PKG-INFO +62 -0
  8. plain_parser-0.1.0/README.md +39 -0
  9. plain_parser-0.1.0/pyproject.toml +68 -0
  10. plain_parser-0.1.0/src/plain_parser/__init__.py +27 -0
  11. plain_parser-0.1.0/src/plain_parser/concept_utils.py +221 -0
  12. plain_parser-0.1.0/src/plain_parser/exceptions.py +39 -0
  13. plain_parser-0.1.0/src/plain_parser/liquid_nodes.py +109 -0
  14. plain_parser-0.1.0/src/plain_parser/loaders.py +101 -0
  15. plain_parser-0.1.0/src/plain_parser/plain_file.py +861 -0
  16. plain_parser-0.1.0/src/plain_parser/plain_spec.py +414 -0
  17. plain_parser-0.1.0/tests/__init__.py +0 -0
  18. plain_parser-0.1.0/tests/conftest.py +31 -0
  19. plain_parser-0.1.0/tests/data/imports/circular_imports_1.plain +8 -0
  20. plain_parser-0.1.0/tests/data/imports/circular_imports_2.plain +8 -0
  21. plain_parser-0.1.0/tests/data/imports/circular_imports_main.plain +12 -0
  22. plain_parser-0.1.0/tests/data/imports/diamond_import_1.plain +16 -0
  23. plain_parser-0.1.0/tests/data/imports/diamond_import_2.plain +16 -0
  24. plain_parser-0.1.0/tests/data/imports/diamond_import_common.plain +11 -0
  25. plain_parser-0.1.0/tests/data/imports/diamond_imports_main.plain +13 -0
  26. plain_parser-0.1.0/tests/data/imports/import_with_functionalities.plain +7 -0
  27. plain_parser-0.1.0/tests/data/imports/imports_module_with_functionalities.plain +12 -0
  28. plain_parser-0.1.0/tests/data/imports/non_existent_import.plain +12 -0
  29. plain_parser-0.1.0/tests/data/plainfile/add_new_task_modal_specification.yaml +1 -0
  30. plain_parser-0.1.0/tests/data/plainfile/duplicate_specification_heading.plain +7 -0
  31. plain_parser-0.1.0/tests/data/plainfile/empty_functional_specs_section.plain +4 -0
  32. plain_parser-0.1.0/tests/data/plainfile/invalid_specification_order.plain +7 -0
  33. plain_parser-0.1.0/tests/data/plainfile/missing_non_functional_requirements.plain +5 -0
  34. plain_parser-0.1.0/tests/data/plainfile/no_functional_specs_section.plain +5 -0
  35. plain_parser-0.1.0/tests/data/plainfile/plain_source_with_absolute_link.plain +9 -0
  36. plain_parser-0.1.0/tests/data/plainfile/plain_source_with_url_link.plain +9 -0
  37. plain_parser-0.1.0/tests/data/plainfile/task_list_ui_specification.yaml +1 -0
  38. plain_parser-0.1.0/tests/data/plainfile/task_manager_with_reference_links.plain +40 -0
  39. plain_parser-0.1.0/tests/data/plainfile/without_non_functional_requirement.plain +5 -0
  40. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +20 -0
  41. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +20 -0
  42. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +18 -0
  43. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +18 -0
  44. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_definition.plain +16 -0
  45. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_noconcepts.plain +18 -0
  46. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_nonconcept.plain +15 -0
  47. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_nondefined.plain +18 -0
  48. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_redefinition.plain +19 -0
  49. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_several_concepts.plain +13 -0
  50. plain_parser-0.1.0/tests/data/plainfileparser/concept_validation_valid.plain +16 -0
  51. plain_parser-0.1.0/tests/data/plainfileparser/cyclic_definitions.plain +9 -0
  52. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts.plain +13 -0
  53. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_base.plain +2 -0
  54. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +13 -0
  55. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_example.plain +13 -0
  56. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_imported_exported.plain +12 -0
  57. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +15 -0
  58. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_malformed.plain +13 -0
  59. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_missing_definition.plain +14 -0
  60. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +13 -0
  61. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +14 -0
  62. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +13 -0
  63. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_nested.plain +15 -0
  64. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_nested_example.plain +13 -0
  65. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_no_definitions.plain +10 -0
  66. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_not_declared.plain +8 -0
  67. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +13 -0
  68. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_transitive_example.plain +13 -0
  69. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +13 -0
  70. plain_parser-0.1.0/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +13 -0
  71. plain_parser-0.1.0/tests/data/plainfileparser/plain_file_parser_with_comments.plain +8 -0
  72. plain_parser-0.1.0/tests/data/plainfileparser/plain_file_with_comments_indented.plain +10 -0
  73. plain_parser-0.1.0/tests/data/plainfileparser/regular_plain_source.plain +11 -0
  74. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts.plain +7 -0
  75. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_defs.plain +2 -0
  76. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_example.plain +13 -0
  77. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_l1.plain +7 -0
  78. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_l2.plain +5 -0
  79. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_missing.plain +10 -0
  80. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_module.plain +11 -0
  81. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_partial.plain +13 -0
  82. plain_parser-0.1.0/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +14 -0
  83. plain_parser-0.1.0/tests/data/plainfileparser/requires_without_definitions_example.plain +10 -0
  84. plain_parser-0.1.0/tests/data/plainfileparser/topological_sort.plain +14 -0
  85. plain_parser-0.1.0/tests/data/plainfileparser/topological_sort_not_referenced.plain +22 -0
  86. plain_parser-0.1.0/tests/data/requires/circular_requires_main.plain +18 -0
  87. plain_parser-0.1.0/tests/data/requires/circular_requires_sub.plain +12 -0
  88. plain_parser-0.1.0/tests/data/requires/diamond_requires_1.plain +12 -0
  89. plain_parser-0.1.0/tests/data/requires/diamond_requires_2.plain +12 -0
  90. plain_parser-0.1.0/tests/data/requires/diamond_requires_common.plain +7 -0
  91. plain_parser-0.1.0/tests/data/requires/diamond_requires_main.plain +19 -0
  92. plain_parser-0.1.0/tests/data/requires/independent_requires_1.plain +7 -0
  93. plain_parser-0.1.0/tests/data/requires/independent_requires_2.plain +7 -0
  94. plain_parser-0.1.0/tests/data/requires/independent_requires_main.plain +19 -0
  95. plain_parser-0.1.0/tests/data/requires/non_existent_require.plain +18 -0
  96. plain_parser-0.1.0/tests/data/requires/normal_requires_1.plain +12 -0
  97. plain_parser-0.1.0/tests/data/requires/normal_requires_2.plain +12 -0
  98. plain_parser-0.1.0/tests/data/requires/normal_requires_common.plain +7 -0
  99. plain_parser-0.1.0/tests/data/requires/normal_requires_main.plain +19 -0
  100. plain_parser-0.1.0/tests/data/sample_base64_image.txt +1 -0
  101. plain_parser-0.1.0/tests/data/simple.plain +7 -0
  102. plain_parser-0.1.0/tests/data/templates/block_level_include.plain +12 -0
  103. plain_parser-0.1.0/tests/data/templates/code_variables.plain +10 -0
  104. plain_parser-0.1.0/tests/data/templates/header.plain +7 -0
  105. plain_parser-0.1.0/tests/data/templates/implement.plain +3 -0
  106. plain_parser-0.1.0/tests/data/templates/implement_2.plain +3 -0
  107. plain_parser-0.1.0/tests/data/templates/template_include.plain +4 -0
  108. plain_parser-0.1.0/tests/data/templates/test_hardest_problem.plain +2 -0
  109. plain_parser-0.1.0/tests/test_exceptions.py +21 -0
  110. plain_parser-0.1.0/tests/test_imports.py +53 -0
  111. plain_parser-0.1.0/tests/test_loaders.py +98 -0
  112. plain_parser-0.1.0/tests/test_plainfile.py +270 -0
  113. plain_parser-0.1.0/tests/test_plainfileparser.py +734 -0
  114. plain_parser-0.1.0/tests/test_plainspec.py +133 -0
  115. plain_parser-0.1.0/tests/test_requires.py +28 -0
@@ -0,0 +1,13 @@
1
+ [flake8]
2
+ max-line-length = 120
3
+ extend-ignore = E203, E501, B001, N803, N806, N813, U101
4
+ exclude =
5
+ .git,
6
+ __pycache__,
7
+ .venv,
8
+ build,
9
+ dist,
10
+ tests
11
+ per-file-ignores =
12
+ __init__.py: F401
13
+ max-complexity = 15
@@ -0,0 +1,44 @@
1
+ name: Code Quality & Tests
2
+
3
+ on:
4
+ # Pushes to PR branches are covered by pull_request; listening to every push
5
+ # too would run the suite twice per commit.
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ check:
13
+ name: Lint and test
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
17
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
18
+ with:
19
+ python-version: "3.11"
20
+ - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
21
+ with:
22
+ path: ~/.cache/pip
23
+ key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
24
+ - name: Install dependencies
25
+ id: install
26
+ run: python -m pip install --upgrade pip && pip install -e ".[dev]"
27
+
28
+ # Each check runs even if an earlier one failed, so one run reports
29
+ # every failure.
30
+ - name: Black Formatting
31
+ if: ${{ !cancelled() && steps.install.outcome == 'success' }}
32
+ run: black . --check
33
+ - name: Import Sorting
34
+ if: ${{ !cancelled() && steps.install.outcome == 'success' }}
35
+ run: isort . --check-only --diff
36
+ - name: Flake8 Linting
37
+ if: ${{ !cancelled() && steps.install.outcome == 'success' }}
38
+ run: flake8 .
39
+ - name: MyPy Type Checking
40
+ if: ${{ !cancelled() && steps.install.outcome == 'success' }}
41
+ run: mypy src
42
+ - name: Tests
43
+ if: ${{ !cancelled() && steps.install.outcome == 'success' }}
44
+ run: pytest tests -v
@@ -0,0 +1,147 @@
1
+ name: Publish dev build to PyPI
2
+
3
+ # Every merge to main publishes a PEP 440 dev release (e.g. 0.1.1.dev7) to
4
+ # PyPI, so the latest main is always installable from the real index with real
5
+ # dependency resolution. Dev releases are pre-releases: pip and uv skip them
6
+ # unless asked, so ordinary installs keep getting the latest stable.
7
+ #
8
+ # Tagged releases (including rc/beta/alpha) are handled by publish-to-pypi.yml.
9
+ on:
10
+ push:
11
+ branches: [main]
12
+ # Ad-hoc runs, e.g. re-publishing after a PyPI outage.
13
+ workflow_dispatch:
14
+
15
+ # Least privilege by default: jobs get no GITHUB_TOKEN scopes unless they
16
+ # opt in below.
17
+ permissions: {}
18
+
19
+ # Serialize publishes so versions land in merge order. Queued runs are not
20
+ # cancelled — each merge should still get its own dev build.
21
+ concurrency:
22
+ group: publish-dev-to-pypi
23
+ cancel-in-progress: false
24
+
25
+ jobs:
26
+ build:
27
+ name: Build dev distribution
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: read
31
+ outputs:
32
+ should-publish: ${{ steps.version.outputs.should-publish }}
33
+ version: ${{ steps.version.outputs.version }}
34
+
35
+ steps:
36
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
37
+ with:
38
+ # hatch-vcs derives the version from git tags, so we need full history.
39
+ fetch-depth: 0
40
+
41
+ - name: Install uv
42
+ # uv provisions its own Python (honoring requires-python), so no
43
+ # separate actions/setup-python step is needed.
44
+ run: |
45
+ curl -LsSf https://astral.sh/uv/install.sh | sh
46
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
47
+
48
+ - name: Compute dev version
49
+ id: version
50
+ # Between releases hatch-vcs produces something like
51
+ # "0.3.10.dev16+gb710c2982". The "+g..." local version segment is valid
52
+ # PEP 440 but is rejected by PyPI on upload, so it has to go. We keep
53
+ # setuptools-scm's own next-version guess (0.3.10 — the patch bump
54
+ # after the latest tag) rather than re-deriving it here, and use the
55
+ # run number as the dev counter: it is strictly increasing, so each
56
+ # merge sorts after the last and re-runs never collide.
57
+ run: |
58
+ RAW=$(uvx --from setuptools-scm python -c \
59
+ "from setuptools_scm import get_version; print(get_version())")
60
+ echo "setuptools-scm reports: $RAW"
61
+
62
+ # On a commit that IS a release tag, setuptools-scm returns a clean
63
+ # version with no .dev component (e.g. "0.3.9" or "0.4.0rc1").
64
+ # Appending ".dev<run>" to that yields 0.3.9.dev42, which sorts BELOW
65
+ # the released 0.3.9 — newer code would look older. A tagged commit
66
+ # is a real release and is published by publish-to-pypi.yml, so there
67
+ # is no dev build to make here.
68
+ case "$RAW" in
69
+ *.dev*) ;;
70
+ *)
71
+ echo "::notice::$RAW is a tagged release; skipping dev build."
72
+ echo "should-publish=false" >> $GITHUB_OUTPUT
73
+ exit 0
74
+ ;;
75
+ esac
76
+
77
+ BASE=${RAW%%+*}
78
+ BASE=${BASE%%.dev*}
79
+ VERSION="${BASE}.dev${GITHUB_RUN_NUMBER}"
80
+ echo "Derived version: $RAW -> $VERSION"
81
+ echo "SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION" >> $GITHUB_ENV
82
+ echo "should-publish=true" >> $GITHUB_OUTPUT
83
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
84
+
85
+ - name: Build package
86
+ if: steps.version.outputs.should-publish == 'true'
87
+ # hatch-vcs reads SETUPTOOLS_SCM_PRETEND_VERSION in place of the tag.
88
+ run: |
89
+ uv build
90
+ echo "Built distributions:"
91
+ ls -l dist/
92
+
93
+ - name: Store the distribution packages
94
+ if: steps.version.outputs.should-publish == 'true'
95
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
96
+ with:
97
+ name: dev-package-distributions
98
+ path: dist/
99
+
100
+ publish-dev:
101
+ name: Publish dev build to PyPI
102
+ needs: build
103
+ if: needs.build.outputs.should-publish == 'true'
104
+ runs-on: ubuntu-latest
105
+ environment:
106
+ name: pypi
107
+ url: https://pypi.org/p/plain-parser
108
+ permissions:
109
+ # Mint a short-lived OIDC token so PyPI Trusted Publishing can
110
+ # authenticate this run — no long-lived API token needed.
111
+ id-token: write
112
+
113
+ env:
114
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
115
+
116
+ steps:
117
+ - name: Download distribution packages
118
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
119
+ with:
120
+ name: dev-package-distributions
121
+ path: dist/
122
+
123
+ - name: Publish to PyPI
124
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
125
+ with:
126
+ # PyPI uploads are immutable, so a re-run of an already-published
127
+ # version should be a no-op, not a red build.
128
+ skip-existing: true
129
+
130
+ - name: Notify Slack (success)
131
+ if: ${{ success() && env.SLACK_WEBHOOK_URL != '' }}
132
+ env:
133
+ VERSION: ${{ needs.build.outputs.version }}
134
+ run: |
135
+ # Spell out the --pre install: a plain "pip install plain-parser" will
136
+ # NOT pick this up, which otherwise reads as a broken publish.
137
+ TEXT="*plain-parser*: dev build \`$VERSION\` published to PyPI :test_tube:. Normal installs are unaffected — try it with \`pip install --pre plain-parser==$VERSION\`."
138
+ curl -s -X POST "$SLACK_WEBHOOK_URL" \
139
+ -H 'Content-type: application/json' \
140
+ --data "$(jq -n --arg t "$TEXT" '{text:$t}')"
141
+
142
+ - name: Notify Slack (failure)
143
+ if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }}
144
+ run: |
145
+ curl -s -X POST "$SLACK_WEBHOOK_URL" \
146
+ -H 'Content-type: application/json' \
147
+ --data "{\"text\":\"*plain-parser*: Publishing dev build to PyPI FAILED :x: (commit \`${{ github.sha }}\`). See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}"
@@ -0,0 +1,117 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ # Least privilege by default: jobs get no GITHUB_TOKEN scopes unless they
8
+ # opt in below.
9
+ permissions: {}
10
+
11
+ jobs:
12
+ build:
13
+ name: Build distribution
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: write
17
+
18
+ env:
19
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
20
+
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
+ with:
24
+ # hatch-vcs derives the version from git tags, so we need full history.
25
+ fetch-depth: 0
26
+
27
+ - name: Notify Slack (start)
28
+ if: ${{ env.SLACK_WEBHOOK_URL != '' }}
29
+ run: |
30
+ curl -s -X POST "$SLACK_WEBHOOK_URL" \
31
+ -H 'Content-type: application/json' \
32
+ --data "{\"text\":\"*plain-parser*: Release \`${{ github.ref_name }}\` published by *${{ github.actor }}* — building and publishing to PyPI...\"}"
33
+
34
+ - name: Install uv
35
+ # uv provisions its own Python (honoring requires-python), so no
36
+ # separate actions/setup-python step is needed.
37
+ run: |
38
+ curl -LsSf https://astral.sh/uv/install.sh | sh
39
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
40
+
41
+ - name: Build package
42
+ # hatch-vcs reads the version straight from the release tag.
43
+ run: |
44
+ uv build
45
+ echo "Built distributions:"
46
+ ls -l dist/
47
+
48
+ - name: Store the distribution packages
49
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
50
+ with:
51
+ name: python-package-distributions
52
+ path: dist/
53
+
54
+ - name: Upload assets to GitHub Release
55
+ env:
56
+ GH_TOKEN: ${{ github.token }}
57
+ run: |
58
+ gh release upload ${{ github.ref_name }} dist/* --clobber
59
+
60
+ - name: Notify Slack (build failure)
61
+ if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }}
62
+ run: |
63
+ curl -s -X POST "$SLACK_WEBHOOK_URL" \
64
+ -H 'Content-type: application/json' \
65
+ --data "{\"text\":\"*plain-parser*: Build of \`${{ github.ref_name }}\` FAILED :x:. Nothing was published to PyPI. See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}"
66
+
67
+ publish-to-pypi:
68
+ name: Publish to PyPI
69
+ needs: build
70
+ runs-on: ubuntu-latest
71
+ environment:
72
+ name: pypi
73
+ url: https://pypi.org/p/plain-parser
74
+ permissions:
75
+ # Mint a short-lived OIDC token so PyPI Trusted Publishing can
76
+ # authenticate this run — no long-lived API token needed.
77
+ id-token: write
78
+
79
+ env:
80
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
81
+
82
+ steps:
83
+ - name: Download distribution packages
84
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
85
+ with:
86
+ name: python-package-distributions
87
+ path: dist/
88
+
89
+ - name: Publish to PyPI
90
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
91
+
92
+ - name: Notify Slack (success)
93
+ if: ${{ success() && env.SLACK_WEBHOOK_URL != '' }}
94
+ env:
95
+ # GitHub marks the release itself as a pre-release; the tag
96
+ # (v0.2.0rc1) carries the same intent for PEP 440.
97
+ IS_PRERELEASE: ${{ github.event.release.prerelease }}
98
+ TAG: ${{ github.ref_name }}
99
+ run: |
100
+ VERSION="${TAG#v}"
101
+ if [ "$IS_PRERELEASE" = "true" ]; then
102
+ # pip and uv skip pre-releases by default, so "--upgrade" would
103
+ # NOT pick this up. Tell people the command that actually works.
104
+ TEXT="*plain-parser*: pre-release \`$TAG\` published to PyPI :test_tube:. Normal installs are unaffected — try it with \`pip install --pre plain-parser==$VERSION\`."
105
+ else
106
+ TEXT="*plain-parser*: \`$TAG\` published to PyPI :white_check_mark:. Install with \`pip install --upgrade plain-parser\`."
107
+ fi
108
+ curl -s -X POST "$SLACK_WEBHOOK_URL" \
109
+ -H 'Content-type: application/json' \
110
+ --data "$(jq -n --arg t "$TEXT" '{text:$t}')"
111
+
112
+ - name: Notify Slack (failure)
113
+ if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }}
114
+ run: |
115
+ curl -s -X POST "$SLACK_WEBHOOK_URL" \
116
+ -H 'Content-type: application/json' \
117
+ --data "{\"text\":\"*plain-parser*: Publishing \`${{ github.ref_name }}\` to PyPI FAILED :x:. See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}"
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Codeplain Inc.
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,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: plain-parser
3
+ Version: 0.1.0
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: networkx<4,>=3.6.1
14
+ Requires-Dist: python-frontmatter<2,>=1.3.0
15
+ Requires-Dist: python-liquid2<0.5,>=0.4.0
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
+ ```python
41
+ from plain_parser import plain_file_parser
42
+
43
+ module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
44
+ ```
45
+
46
+ `plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
47
+ `test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
48
+ entries. `plain_parser.plain_spec` has helpers for walking functionalities
49
+ (`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ uv venv --python 3.11 && source .venv/bin/activate
55
+ uv pip install -e ".[dev]"
56
+ pytest
57
+ black . --check && isort . --check-only && flake8 . && mypy src
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,39 @@
1
+ # plain-parser
2
+
3
+ Python parser for [`***plain`](https://www.plainlang.org/docs/) specification files.
4
+
5
+ Reads a `.plain` module and its `import` / `requires` chain, resolves Liquid templates,
6
+ validates concepts and linked resources, and returns the marshalled specification tree
7
+ plus the ordered list of functionalities (FRIDs).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install plain-parser
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from plain_parser import plain_file_parser
19
+
20
+ module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
21
+ ```
22
+
23
+ `plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
24
+ `test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
25
+ entries. `plain_parser.plain_spec` has helpers for walking functionalities
26
+ (`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
27
+
28
+ ## Development
29
+
30
+ ```bash
31
+ uv venv --python 3.11 && source .venv/bin/activate
32
+ uv pip install -e ".[dev]"
33
+ pytest
34
+ black . --check && isort . --check-only && flake8 . && mypy src
35
+ ```
36
+
37
+ ## License
38
+
39
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "plain-parser"
7
+ dynamic = ["version"]
8
+ description = "Parser for ***plain specification files"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ classifiers = [
14
+ "Intended Audience :: Developers",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Software Development :: Compilers",
18
+ ]
19
+ dependencies = [
20
+ "python-liquid2>=0.4.0,<0.5",
21
+ "mistletoe>=1.6.0,<2",
22
+ "python-frontmatter>=1.3.0,<2",
23
+ "networkx>=3.6.1,<4",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest==9.1.1",
29
+ "flake8==7.3.0",
30
+ "black==26.5.1",
31
+ "isort==8.0.1",
32
+ "mypy==2.3.0",
33
+ ]
34
+
35
+ [tool.hatch.version]
36
+ source = "vcs"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/plain_parser"]
40
+
41
+ [tool.black]
42
+ line-length = 120
43
+ target-version = ['py311']
44
+ extend-exclude = '''
45
+ ^/(.git|.venv|dist|tests/data)/
46
+ '''
47
+
48
+ [tool.isort]
49
+ profile = "black"
50
+ line_length = 120
51
+ skip = [".git", ".venv", "dist", "tests/data"]
52
+ skip_gitignore = true
53
+
54
+ [tool.mypy]
55
+ python_version = "3.11"
56
+ ignore_missing_imports = true
57
+ show_error_codes = true
58
+ disable_error_code = [
59
+ "var-annotated", "import-untyped", "assignment", "return-value", "attr-defined",
60
+ "no-redef", "func-returns-value", "override", "index", "union-attr", "valid-type",
61
+ "misc", "operator", "unreachable", "has-type", "return", "type-arg",
62
+ ]
63
+ exclude = ["^\\.git/", "^\\.venv/", "^tests/"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ python_files = "test_*.py"
68
+ norecursedirs = ["tests/data"]
@@ -0,0 +1,27 @@
1
+ """Parser for ***plain specification files."""
2
+
3
+ from plain_parser import concept_utils, exceptions, loaders, plain_file, plain_spec
4
+ from plain_parser.plain_file import (
5
+ PlainFileParseResult,
6
+ get_filename_from_module_name,
7
+ get_module_name_from_filename,
8
+ marshall_plain_source,
9
+ parse_plain_file,
10
+ parse_plain_source,
11
+ plain_file_parser,
12
+ )
13
+
14
+ __all__ = [
15
+ "PlainFileParseResult",
16
+ "concept_utils",
17
+ "exceptions",
18
+ "get_filename_from_module_name",
19
+ "get_module_name_from_filename",
20
+ "loaders",
21
+ "marshall_plain_source",
22
+ "parse_plain_file",
23
+ "parse_plain_source",
24
+ "plain_file",
25
+ "plain_file_parser",
26
+ "plain_spec",
27
+ ]