pyact-cli 1.1.0__tar.gz → 1.2.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.
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/PKG-INFO +11 -2
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/README.md +10 -1
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pamd_helpers/__init__.py +70 -15
- pyact_cli-1.2.0/pyact/__init__.py +1 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/cli.py +55 -31
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/core.py +150 -44
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact_cli.egg-info/PKG-INFO +11 -2
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/setup.py +1 -1
- pyact_cli-1.1.0/pyact/__init__.py +0 -1
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/conf_parser.py +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/css2json.py +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/linter.py +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/mdTOword.py +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/py2tex.py +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact/scaffold.py +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact_cli.egg-info/SOURCES.txt +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact_cli.egg-info/dependency_links.txt +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact_cli.egg-info/entry_points.txt +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact_cli.egg-info/requires.txt +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/pyact_cli.egg-info/top_level.txt +0 -0
- {pyact_cli-1.1.0 → pyact_cli-1.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyact-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A CLI tool to compile .pamd files to Markdown.
|
|
5
5
|
Home-page: https://github.com/Abstergo2003/PyAct
|
|
6
6
|
Author: Abstergo2003
|
|
@@ -37,7 +37,8 @@ pip install pyact-cli
|
|
|
37
37
|
|
|
38
38
|
## 📖 How it works
|
|
39
39
|
|
|
40
|
-
A `.pamd` file
|
|
40
|
+
A `.pamd` file contains exactly one **Markdown cell** and may contain multiple
|
|
41
|
+
**Python cells**. Python cells are concatenated in order before execution.
|
|
41
42
|
|
|
42
43
|
### 1. Write your Python logic
|
|
43
44
|
Define a `context()` function that returns the variables you want to inject. You can even use the built-in `pamd_helpers` to do heavy lifting!
|
|
@@ -86,6 +87,14 @@ Run the compiler from your terminal to generate your final Markdown document:
|
|
|
86
87
|
pyact path/to/your_file.pamd -o output.md
|
|
87
88
|
```
|
|
88
89
|
|
|
90
|
+
Use `--no-cache` to force a rebuild without reading cached Markdown, or
|
|
91
|
+
`--clear-cache` to remove the project's `.pamd-cache` directory before building:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pyact path/to/your_file.pamd --no-cache
|
|
95
|
+
pyact path/to/your_file.pamd --clear-cache
|
|
96
|
+
```
|
|
97
|
+
|
|
89
98
|
### 5. Compile to Word (DOCX)
|
|
90
99
|
You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
|
|
91
100
|
```bash
|
|
@@ -14,7 +14,8 @@ pip install pyact-cli
|
|
|
14
14
|
|
|
15
15
|
## 📖 How it works
|
|
16
16
|
|
|
17
|
-
A `.pamd` file
|
|
17
|
+
A `.pamd` file contains exactly one **Markdown cell** and may contain multiple
|
|
18
|
+
**Python cells**. Python cells are concatenated in order before execution.
|
|
18
19
|
|
|
19
20
|
### 1. Write your Python logic
|
|
20
21
|
Define a `context()` function that returns the variables you want to inject. You can even use the built-in `pamd_helpers` to do heavy lifting!
|
|
@@ -63,6 +64,14 @@ Run the compiler from your terminal to generate your final Markdown document:
|
|
|
63
64
|
pyact path/to/your_file.pamd -o output.md
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
Use `--no-cache` to force a rebuild without reading cached Markdown, or
|
|
68
|
+
`--clear-cache` to remove the project's `.pamd-cache` directory before building:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pyact path/to/your_file.pamd --no-cache
|
|
72
|
+
pyact path/to/your_file.pamd --clear-cache
|
|
73
|
+
```
|
|
74
|
+
|
|
66
75
|
### 5. Compile to Word (DOCX)
|
|
67
76
|
You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
|
|
68
77
|
```bash
|
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import ast
|
|
3
|
+
import textwrap
|
|
2
4
|
from typing import Callable, List, Any
|
|
3
5
|
from enum import Enum
|
|
4
6
|
from pyact.py2tex import py2tex
|
|
5
7
|
|
|
8
|
+
|
|
9
|
+
def _extract_lambda_parts(func: Callable) -> tuple[str, list[str], str]:
|
|
10
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
11
|
+
tree = ast.parse(source)
|
|
12
|
+
lambda_node = next(
|
|
13
|
+
(node for node in ast.walk(tree) if isinstance(node, ast.Lambda)),
|
|
14
|
+
None,
|
|
15
|
+
)
|
|
16
|
+
if lambda_node is None:
|
|
17
|
+
raise ValueError("Expected a lambda function.")
|
|
18
|
+
|
|
19
|
+
expression = ast.get_source_segment(source, lambda_node.body)
|
|
20
|
+
if expression is None:
|
|
21
|
+
raise ValueError("Could not read the lambda expression.")
|
|
22
|
+
|
|
23
|
+
name = getattr(func, "__name__", "calculation")
|
|
24
|
+
assignment = next(
|
|
25
|
+
(
|
|
26
|
+
node
|
|
27
|
+
for node in ast.walk(tree)
|
|
28
|
+
if isinstance(node, ast.Assign)
|
|
29
|
+
and any(isinstance(target, ast.Name) for target in node.targets)
|
|
30
|
+
and lambda_node in ast.walk(node.value)
|
|
31
|
+
),
|
|
32
|
+
None,
|
|
33
|
+
)
|
|
34
|
+
if assignment is not None:
|
|
35
|
+
name = next(
|
|
36
|
+
target.id
|
|
37
|
+
for target in assignment.targets
|
|
38
|
+
if isinstance(target, ast.Name)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
variables = [argument.arg for argument in lambda_node.args.args]
|
|
42
|
+
return name, variables, expression
|
|
43
|
+
|
|
6
44
|
def equation(func: Callable, values: List[Any]) -> str:
|
|
7
45
|
"""
|
|
8
46
|
Converts a Python lambda or simple function into a LaTeX math equation string.
|
|
@@ -19,12 +57,8 @@ def equation(func: Callable, values: List[Any]) -> str:
|
|
|
19
57
|
str: A Markdown math string formatted as `$$ Name = Equation = Value $$`.
|
|
20
58
|
"""
|
|
21
59
|
print("Function 'equation()' is deprecated and will be removed, please use Calculation() class.")
|
|
22
|
-
source = inspect.getsource(func)
|
|
23
60
|
value = func(*values)
|
|
24
|
-
name,
|
|
25
|
-
print(name, tail)
|
|
26
|
-
head, sep, equ = tail.rpartition(":")
|
|
27
|
-
print(head, equ)
|
|
61
|
+
name, _, equ = _extract_lambda_parts(func)
|
|
28
62
|
|
|
29
63
|
name = name.strip()
|
|
30
64
|
if "_" in name:
|
|
@@ -259,6 +293,9 @@ class Footnote:
|
|
|
259
293
|
self.number = number
|
|
260
294
|
self.text = text
|
|
261
295
|
|
|
296
|
+
def __repr__(self) -> str:
|
|
297
|
+
return self.define_string()
|
|
298
|
+
|
|
262
299
|
def define_string(self) -> str:
|
|
263
300
|
"""
|
|
264
301
|
Outputs (str): The full footnote definition block to place at the bottom of the document.
|
|
@@ -351,12 +388,7 @@ class Calculation:
|
|
|
351
388
|
return Calculation(self.func, values, self.replace, precision)
|
|
352
389
|
|
|
353
390
|
def get_representation(self):
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
name, sep, tail = source.rpartition("=")
|
|
357
|
-
|
|
358
|
-
head, sep, equ = tail.rpartition(":")
|
|
359
|
-
self.vars = head.replace("lambda", "").replace(" ", "").split(",")
|
|
391
|
+
name, self.vars, equ = _extract_lambda_parts(self.func)
|
|
360
392
|
|
|
361
393
|
name = name.strip()
|
|
362
394
|
self.name = f"$ {py2tex(name)} $"
|
|
@@ -415,7 +447,7 @@ class Calculation:
|
|
|
415
447
|
|
|
416
448
|
class Table():
|
|
417
449
|
def __init__(self, headers: List[str], values: List[List[Any]], caption: str, add_no: bool = False, starting_number: int = 1):
|
|
418
|
-
self.headers = headers
|
|
450
|
+
self.headers = headers
|
|
419
451
|
self.values = values
|
|
420
452
|
self.caption = caption
|
|
421
453
|
self.add_no = add_no
|
|
@@ -528,7 +560,30 @@ class CList():
|
|
|
528
560
|
list_string += f"- [ ] {i}\n"
|
|
529
561
|
return list_string
|
|
530
562
|
|
|
563
|
+
# feat:: footnote list
|
|
564
|
+
# class FootnoteList():
|
|
565
|
+
# def __init__(self, footnotes: List[Footnote]):
|
|
566
|
+
# self.footnotes = footnotes
|
|
567
|
+
# self.repr = self.get_adnotations()
|
|
568
|
+
|
|
569
|
+
# def __repr__(self):
|
|
570
|
+
# return self.repr
|
|
571
|
+
|
|
572
|
+
# def append(self, footnote: Footnote|List[Footnote]):
|
|
573
|
+
# if isinstance(footnote, List):
|
|
574
|
+
# self.footnotes = self.footnotes + footnote
|
|
575
|
+
# else:
|
|
576
|
+
# self.footnotes = self.footnotes.append(footnote)
|
|
577
|
+
# def get_adnotations(self):
|
|
578
|
+
# adnots = ""
|
|
579
|
+
# for fn in self.footnotes:
|
|
580
|
+
# adnots = adnots + f"{fn.adnotation}\n"
|
|
581
|
+
|
|
531
582
|
if __name__ == "__main__":
|
|
532
|
-
func_w = lambda x_1, y, z: x_1+y+z
|
|
533
|
-
func_eq = Calculation(func_w, [1,2,3], True)
|
|
534
|
-
print(func_eq.vars)
|
|
583
|
+
# func_w = lambda x_1, y, z: x_1+y+z
|
|
584
|
+
# func_eq = Calculation(func_w, [1,2,3], True)
|
|
585
|
+
# print(func_eq.vars)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
print(["lol4", "lol5"][-1])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.0"
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import sys
|
|
3
|
-
import os
|
|
4
3
|
import time
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from .core import map_content, process_content, build_logs, generate_markdown_toc
|
|
6
7
|
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
def _cache_dependencies(root_dir: Path) -> list[Path]:
|
|
9
|
+
dependencies = [
|
|
10
|
+
root_dir / "global.py",
|
|
11
|
+
root_dir / "pamd.conf",
|
|
12
|
+
root_dir / "style.css",
|
|
13
|
+
]
|
|
14
|
+
helper_dir = Path(__file__).parents[1] / "pamd_helpers"
|
|
15
|
+
if helper_dir.exists():
|
|
16
|
+
dependencies.extend(helper_dir.glob("*.py"))
|
|
17
|
+
return dependencies
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_cache_valid(build_tree, cache_mtime, root_dir=None, dependencies=None):
|
|
21
|
+
root_dir = Path(root_dir) if root_dir is not None else None
|
|
22
|
+
if dependencies is None and root_dir is not None:
|
|
23
|
+
dependencies = _cache_dependencies(root_dir)
|
|
24
|
+
for dependency in dependencies or []:
|
|
25
|
+
if dependency.exists() and dependency.stat().st_mtime > cache_mtime:
|
|
11
26
|
return False
|
|
12
27
|
|
|
13
|
-
file_path =
|
|
14
|
-
if
|
|
28
|
+
file_path = Path(build_tree["source_path"])
|
|
29
|
+
if file_path.exists() and file_path.stat().st_mtime > cache_mtime:
|
|
15
30
|
return False
|
|
16
31
|
for t in build_tree.get("templates", []):
|
|
17
32
|
if not is_cache_valid(t, cache_mtime):
|
|
@@ -55,12 +70,14 @@ def main():
|
|
|
55
70
|
parser.add_argument("--css", help="Optional CSS file path to style the DOCX")
|
|
56
71
|
parser.add_argument("--conf", help="Optional path to pamd.conf configuration file")
|
|
57
72
|
parser.add_argument("--lint", action="store_true", help="Run the linter on the project to check for unused variables and files")
|
|
73
|
+
parser.add_argument("--no-cache", action="store_true", help="Rebuild the document without reading the cache")
|
|
74
|
+
parser.add_argument("--clear-cache", action="store_true", help="Remove the project cache before compiling")
|
|
58
75
|
|
|
59
76
|
args = parser.parse_args()
|
|
60
77
|
|
|
61
|
-
input_path =
|
|
62
|
-
directory =
|
|
63
|
-
filename =
|
|
78
|
+
input_path = Path(args.input).resolve()
|
|
79
|
+
directory = input_path.parent
|
|
80
|
+
filename = input_path.name
|
|
64
81
|
|
|
65
82
|
if filename.endswith(".pamd"):
|
|
66
83
|
filename = filename[:-5]
|
|
@@ -73,18 +90,26 @@ def main():
|
|
|
73
90
|
try:
|
|
74
91
|
build_tree = map_content(filename, directory)
|
|
75
92
|
|
|
76
|
-
cache_dir =
|
|
77
|
-
cache_file =
|
|
93
|
+
cache_dir = directory / ".pamd-cache"
|
|
94
|
+
cache_file = cache_dir / f"{filename}.cache.md"
|
|
78
95
|
use_cache = False
|
|
96
|
+
|
|
97
|
+
if args.clear_cache and cache_dir.exists():
|
|
98
|
+
shutil.rmtree(cache_dir)
|
|
79
99
|
|
|
80
|
-
if
|
|
81
|
-
cache_mtime =
|
|
82
|
-
if is_cache_valid(
|
|
100
|
+
if not args.no_cache and cache_file.exists():
|
|
101
|
+
cache_mtime = cache_file.stat().st_mtime
|
|
102
|
+
if is_cache_valid(
|
|
103
|
+
build_tree,
|
|
104
|
+
cache_mtime,
|
|
105
|
+
directory,
|
|
106
|
+
_cache_dependencies(directory),
|
|
107
|
+
):
|
|
83
108
|
use_cache = True
|
|
84
109
|
|
|
85
110
|
if use_cache:
|
|
86
111
|
print("pamd-cache: Using cached markdown...")
|
|
87
|
-
with open(
|
|
112
|
+
with cache_file.open("r", encoding="utf-8") as f:
|
|
88
113
|
content = f.read()
|
|
89
114
|
else:
|
|
90
115
|
print("pamd-cache: Cache invalid or missing. Rebuilding...")
|
|
@@ -96,14 +121,13 @@ def main():
|
|
|
96
121
|
total_ms = (total_end - total_start) * 1000
|
|
97
122
|
|
|
98
123
|
# Write cache
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
with open(cache_file, "w", encoding="utf-8") as f:
|
|
124
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
with cache_file.open("w", encoding="utf-8") as f:
|
|
102
126
|
f.write(content)
|
|
103
127
|
|
|
104
128
|
# Write build log
|
|
105
|
-
log_file =
|
|
106
|
-
with open(
|
|
129
|
+
log_file = cache_dir / "build.log"
|
|
130
|
+
with log_file.open("w", encoding="utf-8") as f:
|
|
107
131
|
f.write("PyAct Build Log\n")
|
|
108
132
|
f.write("================\n\n")
|
|
109
133
|
f.write("File Execution Times:\n")
|
|
@@ -113,7 +137,7 @@ def main():
|
|
|
113
137
|
|
|
114
138
|
md_output = generate_markdown_toc(content)
|
|
115
139
|
if args.output:
|
|
116
|
-
with
|
|
140
|
+
with Path(args.output).open("w", encoding="utf-8") as f:
|
|
117
141
|
f.write(md_output)
|
|
118
142
|
print(f"Successfully compiled to {args.output}")
|
|
119
143
|
else:
|
|
@@ -127,28 +151,28 @@ def main():
|
|
|
127
151
|
styles = style_parser(args.css)
|
|
128
152
|
else:
|
|
129
153
|
# Try to look for style.css in the active directory
|
|
130
|
-
local_css =
|
|
131
|
-
if
|
|
154
|
+
local_css = directory / "style.css"
|
|
155
|
+
if local_css.exists():
|
|
132
156
|
styles = style_parser(local_css)
|
|
133
157
|
else:
|
|
134
158
|
# Fallback to the default one packaged in pyact
|
|
135
|
-
default_css =
|
|
159
|
+
default_css = Path(__file__).parent / "style.css"
|
|
136
160
|
styles = style_parser(default_css)
|
|
137
161
|
|
|
138
162
|
# Load pamd.conf configuration
|
|
139
163
|
conf = {}
|
|
140
164
|
conf_path = None
|
|
141
165
|
if args.conf:
|
|
142
|
-
conf_path =
|
|
166
|
+
conf_path = Path(args.conf).resolve()
|
|
143
167
|
else:
|
|
144
|
-
local_conf =
|
|
145
|
-
cwd_conf =
|
|
146
|
-
if
|
|
168
|
+
local_conf = directory / "pamd.conf"
|
|
169
|
+
cwd_conf = Path.cwd() / "pamd.conf"
|
|
170
|
+
if local_conf.exists():
|
|
147
171
|
conf_path = local_conf
|
|
148
|
-
elif
|
|
172
|
+
elif cwd_conf.exists():
|
|
149
173
|
conf_path = cwd_conf
|
|
150
174
|
|
|
151
|
-
if conf_path and
|
|
175
|
+
if conf_path and conf_path.exists():
|
|
152
176
|
from .conf_parser import conf_parser
|
|
153
177
|
conf = conf_parser(conf_path)
|
|
154
178
|
|
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import json
|
|
3
3
|
import time
|
|
4
|
-
import os
|
|
5
4
|
import linecache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Mapping
|
|
6
7
|
|
|
7
8
|
build_logs = []
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
def validate_pamd_document(data: Any, file_path: str | Path) -> list[dict]:
|
|
12
|
+
"""Validate the structural contract of a PAMD notebook document."""
|
|
13
|
+
path = Path(file_path)
|
|
14
|
+
if not isinstance(data, dict):
|
|
15
|
+
raise ValueError(f"Invalid PAMD file {path}: the root must be a JSON object.")
|
|
16
|
+
|
|
17
|
+
cells = data.get("cells")
|
|
18
|
+
if not isinstance(cells, list):
|
|
19
|
+
raise ValueError(f"Invalid PAMD file {path}: 'cells' must be a JSON array.")
|
|
20
|
+
|
|
21
|
+
validated_cells = []
|
|
22
|
+
markdown_count = 0
|
|
23
|
+
for index, cell in enumerate(cells):
|
|
24
|
+
if not isinstance(cell, dict):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"Invalid PAMD file {path}: cell {index} must be a JSON object."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
cell_type = cell.get("cell_type")
|
|
30
|
+
if cell_type not in {"code", "markdown"}:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Invalid PAMD file {path}: cell {index} has unsupported "
|
|
33
|
+
f"cell_type {cell_type!r}; expected 'code' or 'markdown'."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
source = cell.get("source")
|
|
37
|
+
if isinstance(source, str):
|
|
38
|
+
normalized_source = source
|
|
39
|
+
elif isinstance(source, list) and all(isinstance(line, str) for line in source):
|
|
40
|
+
normalized_source = "".join(source)
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Invalid PAMD file {path}: cell {index} 'source' must be "
|
|
44
|
+
"a string or an array of strings."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if cell_type == "markdown":
|
|
48
|
+
markdown_count += 1
|
|
49
|
+
validated_cells.append(
|
|
50
|
+
{"cell_type": cell_type, "source": normalized_source}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if markdown_count != 1:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Invalid PAMD file {path}: expected exactly one markdown cell, "
|
|
56
|
+
f"found {markdown_count}."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return validated_cells
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_directory(path: str | Path) -> list:
|
|
10
63
|
"""
|
|
11
64
|
Splits a full file path into its directory component and its filename component.
|
|
12
65
|
|
|
@@ -21,8 +74,8 @@ def get_directory(path: str) -> list:
|
|
|
21
74
|
Outputs:
|
|
22
75
|
list: A two-element list `[directory_path, filename]`.
|
|
23
76
|
"""
|
|
24
|
-
|
|
25
|
-
return [
|
|
77
|
+
path = Path(path)
|
|
78
|
+
return [path.parent, path.name]
|
|
26
79
|
|
|
27
80
|
def read_pamd_cells(file_path: str):
|
|
28
81
|
"""
|
|
@@ -39,26 +92,39 @@ def read_pamd_cells(file_path: str):
|
|
|
39
92
|
Outputs:
|
|
40
93
|
tuple (str, str): A tuple containing `(code_content, markdown_content)`.
|
|
41
94
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
95
|
+
path = Path(file_path)
|
|
96
|
+
try:
|
|
97
|
+
with path.open("r", encoding="utf-8") as f:
|
|
98
|
+
data = json.load(f)
|
|
99
|
+
except json.JSONDecodeError as error:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Invalid PAMD file {path}: malformed JSON at line "
|
|
102
|
+
f"{error.lineno}, column {error.colno}: {error.msg}."
|
|
103
|
+
) from error
|
|
104
|
+
except OSError as error:
|
|
105
|
+
raise OSError(f"Could not read PAMD file {path}: {error}") from error
|
|
106
|
+
|
|
107
|
+
cells = validate_pamd_document(data, path)
|
|
44
108
|
|
|
45
109
|
code_content = ""
|
|
46
110
|
markdown_content = ""
|
|
47
111
|
|
|
48
|
-
for cell in
|
|
49
|
-
cell_type = cell
|
|
50
|
-
source = cell
|
|
51
|
-
|
|
52
|
-
source = "".join(source)
|
|
53
|
-
|
|
112
|
+
for cell in cells:
|
|
113
|
+
cell_type = cell["cell_type"]
|
|
114
|
+
source = cell["source"]
|
|
115
|
+
|
|
54
116
|
if cell_type == "code":
|
|
55
|
-
code_content += source
|
|
117
|
+
code_content += source
|
|
118
|
+
if source and not source.endswith("\n"):
|
|
119
|
+
code_content += "\n"
|
|
56
120
|
elif cell_type == "markdown":
|
|
57
|
-
markdown_content += source
|
|
121
|
+
markdown_content += source
|
|
122
|
+
if source and not source.endswith("\n"):
|
|
123
|
+
markdown_content += "\n"
|
|
58
124
|
|
|
59
125
|
return code_content, markdown_content
|
|
60
126
|
|
|
61
|
-
def get_imports(file: str, code_text: str):
|
|
127
|
+
def get_imports(file: str | Path, code_text: str):
|
|
62
128
|
"""
|
|
63
129
|
Executes the Python code from a .pamd file and retrieves the context variables.
|
|
64
130
|
|
|
@@ -75,7 +141,7 @@ def get_imports(file: str, code_text: str):
|
|
|
75
141
|
Outputs:
|
|
76
142
|
dict: The dictionary of variables returned by the user's `context()` function.
|
|
77
143
|
"""
|
|
78
|
-
filename =
|
|
144
|
+
filename = str(Path(file).with_suffix(".pamd"))
|
|
79
145
|
file_namespace = {"__file__": filename}
|
|
80
146
|
if not code_text.strip():
|
|
81
147
|
raise ValueError(f"The file {filename} needs a 'context()' function but has no code cell.")
|
|
@@ -108,20 +174,20 @@ def get_imports(file: str, code_text: str):
|
|
|
108
174
|
|
|
109
175
|
return result_dict
|
|
110
176
|
|
|
111
|
-
def get_globals(directory: str):
|
|
177
|
+
def get_globals(directory: str | Path):
|
|
112
178
|
"""
|
|
113
179
|
Reads global.py and executes its global_context() function to get global variables.
|
|
114
180
|
"""
|
|
115
|
-
global_path =
|
|
116
|
-
if not
|
|
181
|
+
global_path = Path(directory) / "global.py"
|
|
182
|
+
if not global_path.exists():
|
|
117
183
|
return {}
|
|
118
184
|
|
|
119
|
-
with open(
|
|
185
|
+
with global_path.open("r", encoding="utf-8") as f:
|
|
120
186
|
code_text = f.read()
|
|
121
187
|
|
|
122
|
-
file_namespace = {"__file__": global_path}
|
|
188
|
+
file_namespace = {"__file__": str(global_path)}
|
|
123
189
|
try:
|
|
124
|
-
compiled_code = compile(code_text, global_path, "exec")
|
|
190
|
+
compiled_code = compile(code_text, str(global_path), "exec")
|
|
125
191
|
exec(compiled_code, file_namespace)
|
|
126
192
|
except Exception as e:
|
|
127
193
|
raise RuntimeError(f"Error executing global.py: {e}")
|
|
@@ -178,7 +244,20 @@ def find_templates(text: str):
|
|
|
178
244
|
pattern = r"<tmp>(.*?)</tmp>"
|
|
179
245
|
return re.findall(pattern, text)
|
|
180
246
|
|
|
181
|
-
def
|
|
247
|
+
def _template_path(template: str | Path, directory: str | Path) -> Path:
|
|
248
|
+
template_path = Path(template)
|
|
249
|
+
if template_path.suffix != ".pamd":
|
|
250
|
+
template_path = template_path.with_suffix(".pamd")
|
|
251
|
+
if not template_path.is_absolute():
|
|
252
|
+
template_path = Path(directory) / template_path
|
|
253
|
+
return template_path
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def map_content(
|
|
257
|
+
file: str | Path,
|
|
258
|
+
path: str | Path,
|
|
259
|
+
_active_paths: tuple[Path, ...] = (),
|
|
260
|
+
):
|
|
182
261
|
"""
|
|
183
262
|
Recursively maps the dependency tree of a `.pamd` file.
|
|
184
263
|
|
|
@@ -195,32 +274,42 @@ def map_content(file: str, path: str):
|
|
|
195
274
|
dict: A recursive "build tree" dictionary containing metadata, required imports,
|
|
196
275
|
and the build trees of all nested templates.
|
|
197
276
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
code_text, main_file = read_pamd_cells(
|
|
277
|
+
source_path = _template_path(file, path).resolve()
|
|
278
|
+
if source_path in _active_paths:
|
|
279
|
+
cycle_start = _active_paths.index(source_path)
|
|
280
|
+
cycle = (*_active_paths[cycle_start:], source_path)
|
|
281
|
+
cycle_display = " -> ".join(str(item) for item in cycle)
|
|
282
|
+
raise ValueError(f"Recursive <tmp> template cycle detected: {cycle_display}")
|
|
283
|
+
|
|
284
|
+
code_text, main_file = read_pamd_cells(str(source_path))
|
|
206
285
|
templates = find_templates(main_file)
|
|
207
286
|
imports = find_imports(main_file)
|
|
208
287
|
globals_req = find_globals(main_file)
|
|
209
|
-
meta = get_directory(
|
|
288
|
+
meta = get_directory(source_path.with_suffix(""))
|
|
210
289
|
|
|
211
290
|
ready_t = []
|
|
212
291
|
for i in templates:
|
|
213
|
-
ready_t.append(map_content(i, meta[0]))
|
|
292
|
+
ready_t.append(map_content(i, meta[0], (*_active_paths, source_path)))
|
|
214
293
|
|
|
215
294
|
return {
|
|
216
|
-
"tag_name": file,
|
|
217
|
-
"name": meta[1],
|
|
295
|
+
"tag_name": str(file),
|
|
296
|
+
"name": str(meta[1]),
|
|
218
297
|
"path": meta[0],
|
|
298
|
+
"source_path": source_path,
|
|
219
299
|
"imports": imports,
|
|
220
300
|
"globals": globals_req,
|
|
221
301
|
"templates": ready_t
|
|
222
302
|
}
|
|
223
303
|
|
|
304
|
+
def _resolve_value(values: Mapping[str, Any], key: str) -> Any:
|
|
305
|
+
current: Any = values
|
|
306
|
+
for part in key.split("."):
|
|
307
|
+
if not isinstance(current, Mapping) or part not in current:
|
|
308
|
+
raise KeyError(key)
|
|
309
|
+
current = current[part]
|
|
310
|
+
return current
|
|
311
|
+
|
|
312
|
+
|
|
224
313
|
def process_content(build_tree: dict, globals_dict: dict = None):
|
|
225
314
|
"""
|
|
226
315
|
Compiles a mapped build tree into a final flat Markdown string.
|
|
@@ -230,21 +319,38 @@ def process_content(build_tree: dict, globals_dict: dict = None):
|
|
|
230
319
|
if globals_dict is None:
|
|
231
320
|
globals_dict = get_globals(build_tree.get("path", ""))
|
|
232
321
|
|
|
233
|
-
|
|
234
|
-
|
|
322
|
+
source_path = build_tree.get("source_path")
|
|
323
|
+
if source_path is None:
|
|
324
|
+
source_path = _template_path(
|
|
325
|
+
build_tree.get("name", ""),
|
|
326
|
+
build_tree.get("path", ""),
|
|
327
|
+
)
|
|
328
|
+
source_path = Path(source_path)
|
|
329
|
+
code_text, file_text = read_pamd_cells(str(source_path))
|
|
235
330
|
|
|
236
331
|
needed_imports = build_tree.get("imports", [])
|
|
237
332
|
if needed_imports:
|
|
238
|
-
imports = get_imports(
|
|
333
|
+
imports = get_imports(source_path, code_text)
|
|
239
334
|
for i in needed_imports:
|
|
240
|
-
|
|
335
|
+
try:
|
|
336
|
+
value = _resolve_value(imports, i)
|
|
337
|
+
except KeyError:
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"Context variable '{i}' requested in {source_path.name} "
|
|
340
|
+
"but not returned by context()."
|
|
341
|
+
) from None
|
|
342
|
+
file_text = file_text.replace(f"<ctx>{i}</ctx>", str(value))
|
|
241
343
|
|
|
242
344
|
needed_globals = build_tree.get("globals", [])
|
|
243
345
|
for g in needed_globals:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
raise ValueError(
|
|
346
|
+
try:
|
|
347
|
+
value = _resolve_value(globals_dict, g)
|
|
348
|
+
except KeyError:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"Global variable '{g}' requested via <glb> but not found "
|
|
351
|
+
"in global.py's global_context()."
|
|
352
|
+
) from None
|
|
353
|
+
file_text = file_text.replace(f"<glb>{g}</glb>", str(value))
|
|
248
354
|
|
|
249
355
|
for i in build_tree.get("templates", []):
|
|
250
356
|
file_text = file_text.replace(f"<tmp>{i.get('tag_name')}</tmp>", process_content(i, globals_dict))
|
|
@@ -253,7 +359,7 @@ def process_content(build_tree: dict, globals_dict: dict = None):
|
|
|
253
359
|
elapsed_ms = (end_time - start_time) * 1000
|
|
254
360
|
|
|
255
361
|
build_logs.append({
|
|
256
|
-
"file":
|
|
362
|
+
"file": source_path.name,
|
|
257
363
|
"time_ms": elapsed_ms
|
|
258
364
|
})
|
|
259
365
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyact-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A CLI tool to compile .pamd files to Markdown.
|
|
5
5
|
Home-page: https://github.com/Abstergo2003/PyAct
|
|
6
6
|
Author: Abstergo2003
|
|
@@ -37,7 +37,8 @@ pip install pyact-cli
|
|
|
37
37
|
|
|
38
38
|
## 📖 How it works
|
|
39
39
|
|
|
40
|
-
A `.pamd` file
|
|
40
|
+
A `.pamd` file contains exactly one **Markdown cell** and may contain multiple
|
|
41
|
+
**Python cells**. Python cells are concatenated in order before execution.
|
|
41
42
|
|
|
42
43
|
### 1. Write your Python logic
|
|
43
44
|
Define a `context()` function that returns the variables you want to inject. You can even use the built-in `pamd_helpers` to do heavy lifting!
|
|
@@ -86,6 +87,14 @@ Run the compiler from your terminal to generate your final Markdown document:
|
|
|
86
87
|
pyact path/to/your_file.pamd -o output.md
|
|
87
88
|
```
|
|
88
89
|
|
|
90
|
+
Use `--no-cache` to force a rebuild without reading cached Markdown, or
|
|
91
|
+
`--clear-cache` to remove the project's `.pamd-cache` directory before building:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pyact path/to/your_file.pamd --no-cache
|
|
95
|
+
pyact path/to/your_file.pamd --clear-cache
|
|
96
|
+
```
|
|
97
|
+
|
|
89
98
|
### 5. Compile to Word (DOCX)
|
|
90
99
|
You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
|
|
91
100
|
```bash
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|