dp_wizard_templates 0.7.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dp_wizard_templates/VERSION +1 -0
- dp_wizard_templates/__init__.py +5 -0
- dp_wizard_templates/code_template.py +305 -0
- dp_wizard_templates/converters.py +140 -0
- dp_wizard_templates/py.typed +0 -0
- dp_wizard_templates-0.7.0.dist-info/METADATA +61 -0
- dp_wizard_templates-0.7.0.dist-info/RECORD +9 -0
- dp_wizard_templates-0.7.0.dist-info/WHEEL +5 -0
- dp_wizard_templates-0.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.7.0
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
import black
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TemplateException(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_body(func):
|
|
15
|
+
|
|
16
|
+
source_lines = inspect.getsource(func).splitlines()
|
|
17
|
+
first_line = source_lines[0]
|
|
18
|
+
if not re.match(r"def \w+\((\w+(, \w+)*)?\):", first_line.strip()):
|
|
19
|
+
# Parsing to AST and unparsing is a more robust option,
|
|
20
|
+
# but more complicated.
|
|
21
|
+
raise TemplateException(
|
|
22
|
+
f"def and parameters should fit on one line: {first_line}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# The "def" should not be in the output,
|
|
26
|
+
# and cleandoc handles the first line differently.
|
|
27
|
+
source_lines[0] = ""
|
|
28
|
+
body = inspect.cleandoc("\n".join(source_lines))
|
|
29
|
+
comments_to_strip = [
|
|
30
|
+
r"\s+#\s+type:\s+ignore\s*",
|
|
31
|
+
r"\s+#\s+noqa:.+\s*",
|
|
32
|
+
r"\s+#\s+pragma:\s+no cover\s*",
|
|
33
|
+
]
|
|
34
|
+
for comment_re in comments_to_strip:
|
|
35
|
+
body = re.sub(
|
|
36
|
+
comment_re,
|
|
37
|
+
"\n",
|
|
38
|
+
body,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return body
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_repr(value):
|
|
45
|
+
"""
|
|
46
|
+
Confirms that the string returned by repr()
|
|
47
|
+
can be evaluated to recreate the original value.
|
|
48
|
+
Takes a conservative approach by checking
|
|
49
|
+
if the value can be serialized to JSON.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
json.dumps(value)
|
|
53
|
+
except TypeError as e:
|
|
54
|
+
raise TemplateException(e)
|
|
55
|
+
return repr(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_slot_re = r"\b[A-Z][A-Z_]{2,}\b"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _check_kwargs(func):
|
|
62
|
+
def wrapper(*args, **kwargs):
|
|
63
|
+
WHEN = "when"
|
|
64
|
+
errors = []
|
|
65
|
+
for k in kwargs.keys():
|
|
66
|
+
if k in args[0]._ignore:
|
|
67
|
+
errors.append(f'kwarg "{k}" is an ignored slot name')
|
|
68
|
+
if not (re.fullmatch(_slot_re, k) or k == WHEN):
|
|
69
|
+
errors.append(f'kwarg "{k}" is not a valid slot name')
|
|
70
|
+
if errors:
|
|
71
|
+
raise TemplateException(
|
|
72
|
+
"; ".join(errors)
|
|
73
|
+
+ f'. Slots should match "{_slot_re}". '
|
|
74
|
+
+ "Some slots are ignored, and should not be filled: "
|
|
75
|
+
+ ",".join(f'"{v}"' for v in args[0]._ignore)
|
|
76
|
+
)
|
|
77
|
+
if not kwargs.get(WHEN, True):
|
|
78
|
+
# return self:
|
|
79
|
+
return args[0]
|
|
80
|
+
kwargs.pop(WHEN, None)
|
|
81
|
+
return func(*args, **kwargs)
|
|
82
|
+
|
|
83
|
+
return wrapper
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Template:
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
template: str | Callable,
|
|
91
|
+
root: Optional[Path] = None,
|
|
92
|
+
ignore: Iterable[str] = ("TODO",),
|
|
93
|
+
):
|
|
94
|
+
if root is None:
|
|
95
|
+
if callable(template):
|
|
96
|
+
self._source = "function template"
|
|
97
|
+
self._template = _get_body(template)
|
|
98
|
+
else:
|
|
99
|
+
self._source = "string template"
|
|
100
|
+
self._template = template
|
|
101
|
+
else:
|
|
102
|
+
if callable(template):
|
|
103
|
+
raise TemplateException(
|
|
104
|
+
"If template is function, root kwarg not allowed"
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
template_name = f"_{template}.py"
|
|
108
|
+
template_path = root / template_name
|
|
109
|
+
self._source = f"'{template_name}'"
|
|
110
|
+
self._template = template_path.read_text()
|
|
111
|
+
# We want a list of the initial slots, because substitutions
|
|
112
|
+
# can produce sequences of upper case letters that could be mistaken for slots.
|
|
113
|
+
self._initial_slots = self._find_slots()
|
|
114
|
+
self._ignore = ignore
|
|
115
|
+
|
|
116
|
+
def _find_slots(self) -> set[str]:
|
|
117
|
+
# Slots:
|
|
118
|
+
# - are all caps or underscores
|
|
119
|
+
# - have word boundary on either side
|
|
120
|
+
# - are at least three characters
|
|
121
|
+
return set(re.findall(_slot_re, self._template))
|
|
122
|
+
|
|
123
|
+
def _make_message(self, errors: list[str]) -> str:
|
|
124
|
+
return (
|
|
125
|
+
f"In {self._source}, " + ", ".join(sorted(errors)) + f":\n{self._template}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _loop_kwargs(
|
|
129
|
+
self,
|
|
130
|
+
function: Callable[[str, str, list[str]], None],
|
|
131
|
+
**kwargs,
|
|
132
|
+
) -> None:
|
|
133
|
+
errors = []
|
|
134
|
+
for k, v in kwargs.items():
|
|
135
|
+
function(k, v, errors)
|
|
136
|
+
if errors:
|
|
137
|
+
raise TemplateException(self._make_message(errors))
|
|
138
|
+
|
|
139
|
+
def _fill_inline_slots(
|
|
140
|
+
self,
|
|
141
|
+
stringifier: Callable[[str], str],
|
|
142
|
+
**kwargs,
|
|
143
|
+
) -> None:
|
|
144
|
+
def function(k, v, errors):
|
|
145
|
+
k_re = re.escape(k)
|
|
146
|
+
self._template, count = re.subn(
|
|
147
|
+
rf"\b{k_re}\b", stringifier(v), self._template
|
|
148
|
+
)
|
|
149
|
+
if count == 0:
|
|
150
|
+
errors.append(f"no '{k}' slot to fill with '{v}'")
|
|
151
|
+
|
|
152
|
+
self._loop_kwargs(function, **kwargs)
|
|
153
|
+
|
|
154
|
+
def _fill_attribute_slots(self, **kwargs) -> None:
|
|
155
|
+
def function(k, v, errors):
|
|
156
|
+
k_re = re.escape(k)
|
|
157
|
+
attr_re = rf"\.\b{k_re}\b"
|
|
158
|
+
self._template, count = re.subn(
|
|
159
|
+
attr_re, f".{v}" if v else "", self._template
|
|
160
|
+
)
|
|
161
|
+
if count == 0:
|
|
162
|
+
errors.append(
|
|
163
|
+
f"no '.{k}' slot to fill with '{v}'"
|
|
164
|
+
if v
|
|
165
|
+
else f"no '.{k}' slot to delete (because replacement is false-y)"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
self._loop_kwargs(function, **kwargs)
|
|
169
|
+
|
|
170
|
+
def _fill_argument_slots(
|
|
171
|
+
self,
|
|
172
|
+
stringifier: Callable[[str], str],
|
|
173
|
+
**kwargs,
|
|
174
|
+
) -> None:
|
|
175
|
+
def function(k, v, errors):
|
|
176
|
+
k_re = re.escape(k)
|
|
177
|
+
arg_re = rf"\s*\b{k_re}\b,\s*"
|
|
178
|
+
self._template, count = re.subn(
|
|
179
|
+
arg_re, f"{stringifier(v)}," if v else "", self._template
|
|
180
|
+
)
|
|
181
|
+
if count == 0:
|
|
182
|
+
errors.append(
|
|
183
|
+
f"no '{k},' slot to fill with '{v}'"
|
|
184
|
+
if v
|
|
185
|
+
else f"no '{k},' slot to delete (because replacement is false-y)"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self._loop_kwargs(function, **kwargs)
|
|
189
|
+
|
|
190
|
+
def _fill_block_slots(
|
|
191
|
+
self,
|
|
192
|
+
prefix_re: str,
|
|
193
|
+
splitter: Callable[[str], list[str]],
|
|
194
|
+
**kwargs,
|
|
195
|
+
) -> None:
|
|
196
|
+
def function(k, v, errors):
|
|
197
|
+
if not isinstance(v, str):
|
|
198
|
+
errors.append(f"for '{k}' slot, expected string, not '{v}'")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
def match_indent(match):
|
|
202
|
+
# This does what we want, but binding is confusing.
|
|
203
|
+
return "\n".join(
|
|
204
|
+
match.group(1) + line for line in splitter(v) # noqa: B023
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
k_re = re.escape(k)
|
|
208
|
+
self._template, count = re.subn(
|
|
209
|
+
rf"^([ \t]*{prefix_re}){k_re}$",
|
|
210
|
+
match_indent,
|
|
211
|
+
self._template,
|
|
212
|
+
flags=re.MULTILINE,
|
|
213
|
+
)
|
|
214
|
+
if count == 0:
|
|
215
|
+
base_message = f"no '{k}' slot to fill with '{v}'"
|
|
216
|
+
if k in self._template:
|
|
217
|
+
note = (
|
|
218
|
+
"comment slots must be prefixed with '#'"
|
|
219
|
+
if prefix_re
|
|
220
|
+
else "block slots must be alone on line"
|
|
221
|
+
)
|
|
222
|
+
errors.append(f"{base_message} ({note})")
|
|
223
|
+
else:
|
|
224
|
+
errors.append(base_message)
|
|
225
|
+
|
|
226
|
+
self._loop_kwargs(function, **kwargs)
|
|
227
|
+
|
|
228
|
+
@_check_kwargs
|
|
229
|
+
def fill_expressions(self, **kwargs) -> "Template":
|
|
230
|
+
"""
|
|
231
|
+
Fill in variable names, or dicts or lists represented as strings.
|
|
232
|
+
"""
|
|
233
|
+
self._fill_inline_slots(stringifier=str, **kwargs)
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
@_check_kwargs
|
|
237
|
+
def fill_attributes(self, **kwargs) -> "Template":
|
|
238
|
+
"""
|
|
239
|
+
Fill in attributes with expressions, or remove leading "." if false-y.
|
|
240
|
+
"""
|
|
241
|
+
self._fill_attribute_slots(**kwargs)
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
@_check_kwargs
|
|
245
|
+
def fill_argument_expressions(self, **kwargs) -> "Template":
|
|
246
|
+
"""
|
|
247
|
+
Fill in argument expressions, or removing trailing "," if false-y.
|
|
248
|
+
"""
|
|
249
|
+
self._fill_argument_slots(stringifier=str, **kwargs)
|
|
250
|
+
return self
|
|
251
|
+
|
|
252
|
+
@_check_kwargs
|
|
253
|
+
def fill_argument_values(self, **kwargs) -> "Template":
|
|
254
|
+
"""
|
|
255
|
+
Fill in argument values, or removing trailing "," if false-y.
|
|
256
|
+
"""
|
|
257
|
+
self._fill_argument_slots(stringifier=_check_repr, **kwargs)
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
@_check_kwargs
|
|
261
|
+
def fill_values(self, **kwargs) -> "Template":
|
|
262
|
+
"""
|
|
263
|
+
Fill in string or numeric values. `repr` is called before filling.
|
|
264
|
+
"""
|
|
265
|
+
self._fill_inline_slots(stringifier=_check_repr, **kwargs)
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
@_check_kwargs
|
|
269
|
+
def fill_code_blocks(self, **kwargs) -> "Template":
|
|
270
|
+
"""
|
|
271
|
+
Fill in code blocks. Slot must be alone on line.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def splitter(s):
|
|
275
|
+
return s.split("\n")
|
|
276
|
+
|
|
277
|
+
self._fill_block_slots(prefix_re=r"", splitter=splitter, **kwargs)
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
@_check_kwargs
|
|
281
|
+
def fill_comment_blocks(self, **kwargs) -> "Template":
|
|
282
|
+
"""
|
|
283
|
+
Fill in comment blocks. Slot must be commented.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def splitter(s):
|
|
287
|
+
stripped = [line.strip() for line in s.split("\n")]
|
|
288
|
+
return [line for line in stripped if line]
|
|
289
|
+
|
|
290
|
+
self._fill_block_slots(prefix_re=r"#\s+", splitter=splitter, **kwargs)
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def finish(self, reformat: bool = False) -> str:
|
|
294
|
+
# The reformat default is False here,
|
|
295
|
+
# because it is true downstream for notebook generation,
|
|
296
|
+
# and we don't need to be redundant.
|
|
297
|
+
unfilled_slots = (self._initial_slots & self._find_slots()) - set(self._ignore)
|
|
298
|
+
if unfilled_slots:
|
|
299
|
+
errors = [f"'{slot}' slot not filled" for slot in unfilled_slots]
|
|
300
|
+
raise TemplateException(self._make_message(errors))
|
|
301
|
+
|
|
302
|
+
if reformat:
|
|
303
|
+
self._template = black.format_str(self._template, mode=black.Mode())
|
|
304
|
+
|
|
305
|
+
return self._template
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from sys import executable
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
|
|
8
|
+
import black
|
|
9
|
+
import jupytext
|
|
10
|
+
import nbconvert
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_kernel_installed() -> bool:
|
|
14
|
+
try:
|
|
15
|
+
# This method isn't well documented, so it may be fragile.
|
|
16
|
+
jupytext.kernels.kernelspec_from_language("python") # type: ignore
|
|
17
|
+
return True
|
|
18
|
+
except ValueError: # pragma: no cover
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ConversionException(Exception):
|
|
24
|
+
command: str
|
|
25
|
+
stderr: str
|
|
26
|
+
|
|
27
|
+
def __str__(self):
|
|
28
|
+
return f"Script to notebook conversion failed: {self.command}\n{self.stderr})"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def convert_py_to_nb(
|
|
32
|
+
python_str: str, title: str, execute: bool = False, reformat: bool = True
|
|
33
|
+
) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Given Python code as a string, returns a notebook as a string.
|
|
36
|
+
Calls jupytext as a subprocess:
|
|
37
|
+
Not ideal, but only the CLI is documented well.
|
|
38
|
+
"""
|
|
39
|
+
with TemporaryDirectory() as temp_dir:
|
|
40
|
+
if not _is_kernel_installed():
|
|
41
|
+
subprocess.run( # pragma: no cover
|
|
42
|
+
[executable]
|
|
43
|
+
+ "-m ipykernel install --name kernel_name --user".split(" "),
|
|
44
|
+
check=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
temp_dir_path = Path(temp_dir)
|
|
48
|
+
py_path = temp_dir_path / "input.py"
|
|
49
|
+
if reformat:
|
|
50
|
+
# Line length determined by PDF rendering.
|
|
51
|
+
python_str = black.format_str(python_str, mode=black.Mode(line_length=74))
|
|
52
|
+
py_path.write_text(python_str)
|
|
53
|
+
|
|
54
|
+
argv = [executable] + "-m jupytext --from .py --to .ipynb --output -".split(" ")
|
|
55
|
+
if execute:
|
|
56
|
+
argv.append("--execute")
|
|
57
|
+
argv.append(str(py_path.absolute())) # type: ignore
|
|
58
|
+
result = subprocess.run(argv, text=True, capture_output=True)
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
# If there is an error, we want a copy of the file that will stay around,
|
|
61
|
+
# outside the "with TemporaryDirectory()" block.
|
|
62
|
+
# The command we show in the error message isn't exactly what was run,
|
|
63
|
+
# but it should reproduce the error.
|
|
64
|
+
debug_path = Path("/tmp/script.py")
|
|
65
|
+
debug_path.write_text(python_str)
|
|
66
|
+
argv.pop()
|
|
67
|
+
argv.append(str(debug_path)) # type: ignore
|
|
68
|
+
raise ConversionException(command=" ".join(argv), stderr=result.stderr)
|
|
69
|
+
nb_dict = json.loads(result.stdout.strip())
|
|
70
|
+
nb_dict["metadata"]["title"] = title
|
|
71
|
+
return _clean_nb(json.dumps(nb_dict))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _stable_hash(lines: list[str]) -> str:
|
|
75
|
+
import hashlib
|
|
76
|
+
|
|
77
|
+
return hashlib.sha1("\n".join(lines).encode()).hexdigest()[:8]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _clean_nb(nb_json: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Given a notebook as a string of JSON, remove the coda and pip output.
|
|
83
|
+
(The code may produce reports that we do need,
|
|
84
|
+
but the code isn't actually interesting to end users.)
|
|
85
|
+
"""
|
|
86
|
+
nb = json.loads(nb_json)
|
|
87
|
+
new_cells = []
|
|
88
|
+
for cell in nb["cells"]:
|
|
89
|
+
if "pip install" in cell["source"][0]:
|
|
90
|
+
cell["outputs"] = []
|
|
91
|
+
# "Coda" may, or may not be followed by "\n".
|
|
92
|
+
# Be flexible!
|
|
93
|
+
if any(line.startswith("# Coda") for line in cell["source"]):
|
|
94
|
+
break
|
|
95
|
+
# Make ID stable:
|
|
96
|
+
cell["id"] = _stable_hash(cell["source"])
|
|
97
|
+
# Delete execution metadata:
|
|
98
|
+
try:
|
|
99
|
+
del cell["metadata"]["execution"]
|
|
100
|
+
except KeyError:
|
|
101
|
+
pass
|
|
102
|
+
new_cells.append(cell)
|
|
103
|
+
nb["cells"] = new_cells
|
|
104
|
+
return json.dumps(nb, indent=1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_nb_to_html(python_nb: str, numbered=True) -> str:
|
|
108
|
+
import warnings
|
|
109
|
+
|
|
110
|
+
import nbformat.warnings
|
|
111
|
+
|
|
112
|
+
with warnings.catch_warnings():
|
|
113
|
+
warnings.simplefilter(
|
|
114
|
+
action="ignore", category=nbformat.warnings.DuplicateCellId
|
|
115
|
+
)
|
|
116
|
+
notebook = nbformat.reads(python_nb, as_version=4)
|
|
117
|
+
exporter = nbconvert.HTMLExporter(
|
|
118
|
+
template_name="lab",
|
|
119
|
+
# The "classic" template's CSS forces large code cells on to
|
|
120
|
+
# the next page rather than breaking, so use "lab" instead.
|
|
121
|
+
#
|
|
122
|
+
# If you want to tweak the CSS, enable this block and make changes
|
|
123
|
+
# in nbconvert_templates/custom:
|
|
124
|
+
#
|
|
125
|
+
# template_name="custom",
|
|
126
|
+
# extra_template_basedirs=[
|
|
127
|
+
# str((Path(__file__).parent / "nbconvert_templates").absolute())
|
|
128
|
+
# ],
|
|
129
|
+
)
|
|
130
|
+
(body, _resources) = exporter.from_notebook_node(notebook)
|
|
131
|
+
if not numbered:
|
|
132
|
+
body = body.replace(
|
|
133
|
+
"</head>",
|
|
134
|
+
"""
|
|
135
|
+
<style>
|
|
136
|
+
.jp-InputPrompt {display: none;}
|
|
137
|
+
</style>
|
|
138
|
+
</head>""",
|
|
139
|
+
)
|
|
140
|
+
return body
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dp_wizard_templates
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Code templating tools
|
|
5
|
+
Author-email: The OpenDP Project <info@opendp.org>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: black
|
|
9
|
+
Requires-Dist: ipykernel
|
|
10
|
+
Requires-Dist: jupyter-client
|
|
11
|
+
Requires-Dist: jupytext
|
|
12
|
+
Requires-Dist: nbconvert
|
|
13
|
+
Project-URL: GitHub, https://github.com/opendp/dp-wizard-templates
|
|
14
|
+
Project-URL: Homepage, https://opendp.github.io/dp-wizard-templates
|
|
15
|
+
|
|
16
|
+
# DP Wizard Templates
|
|
17
|
+
|
|
18
|
+
[](https://pypi.org/project/dp_wizard_templates/)
|
|
19
|
+
|
|
20
|
+
DP Wizard Templates lets you use syntactically valid Python code as a template.
|
|
21
|
+
Templates can be filled and composed to generate entire notebooks.
|
|
22
|
+
|
|
23
|
+
See the [documentation](https://opendp.github.io/dp-wizard-templates) for more information.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
### Getting Started
|
|
29
|
+
|
|
30
|
+
On MacOS:
|
|
31
|
+
```shell
|
|
32
|
+
$ git clone https://github.com/opendp/dp-wizard-templates.git
|
|
33
|
+
$ cd dp-wizard-templates
|
|
34
|
+
$ brew install python@3.10
|
|
35
|
+
$ python3.10 -m venv .venv
|
|
36
|
+
$ source .venv/bin/activate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You can now install dependencies:
|
|
40
|
+
```shell
|
|
41
|
+
$ pip install -r requirements-dev.txt
|
|
42
|
+
$ pre-commit install
|
|
43
|
+
$ flit install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Tests should pass, and code coverage should be complete (except blocks we explicitly ignore):
|
|
47
|
+
```shell
|
|
48
|
+
$ scripts/ci.sh
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Release
|
|
52
|
+
|
|
53
|
+
- Make sure you're up to date, and have the git-ignored credentials file `.pypirc`.
|
|
54
|
+
- Make one last feature branch with the new version number in the name:
|
|
55
|
+
- Run `scripts/changelog.py` to update the `CHANGELOG.md`.
|
|
56
|
+
- Review the updates and pull a couple highlights to the top.
|
|
57
|
+
- Bump `dp_wizard/VERSION`, and add the new number at the top of the `CHANGELOG.md`.
|
|
58
|
+
- Commit your changes, make a PR, and merge this branch to main.
|
|
59
|
+
- Update `main` with the latest changes: `git checkout main; git pull`
|
|
60
|
+
- Publish: `flit publish --pypirc .pypirc`
|
|
61
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
dp_wizard_templates/VERSION,sha256=sMsnbN5Rd3nKQ6X4_pStzWsco7Tn2l6X8szZw7Q4Hfg,5
|
|
2
|
+
dp_wizard_templates/__init__.py,sha256=DF2xPO1uVitkB9NRBpqd_HayLHk4hnckLo8jaccoEbI,125
|
|
3
|
+
dp_wizard_templates/code_template.py,sha256=GmKOSfU7eOX9QN9PJ0zq1nXUHSTePyn2wM5Wmhjda-g,9687
|
|
4
|
+
dp_wizard_templates/converters.py,sha256=Xzb0suMDCMpqLytgFUtlW80GKlVWRbzfTtfsD6cL8Uo,4548
|
|
5
|
+
dp_wizard_templates/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dp_wizard_templates-0.7.0.dist-info/licenses/LICENSE,sha256=FDrIMeZPiT4g_4w0i1Ec4Bc8h9wfNytroheQN4508yU,1063
|
|
7
|
+
dp_wizard_templates-0.7.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
|
|
8
|
+
dp_wizard_templates-0.7.0.dist-info/METADATA,sha256=y0A2fFxjkUuzYX-CQDyUWnipDjRTMA6k-wry0PJyS5w,1881
|
|
9
|
+
dp_wizard_templates-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenDP
|
|
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.
|