worklog-opsdevnz 0.1.3__tar.gz → 0.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.
Files changed (21) hide show
  1. {worklog_opsdevnz-0.1.3/src/worklog_opsdevnz.egg-info → worklog_opsdevnz-0.2.0}/PKG-INFO +2 -1
  2. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/pyproject.toml +2 -1
  3. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz/template.py +37 -10
  4. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0/src/worklog_opsdevnz.egg-info}/PKG-INFO +2 -1
  5. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz.egg-info/requires.txt +1 -0
  6. worklog_opsdevnz-0.2.0/tests/test_template.py +285 -0
  7. worklog_opsdevnz-0.1.3/tests/test_template.py +0 -99
  8. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/LICENSE +0 -0
  9. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/README.md +0 -0
  10. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/setup.cfg +0 -0
  11. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz/__init__.py +0 -0
  12. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz/cli.py +0 -0
  13. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz/config.py +0 -0
  14. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz/paths.py +0 -0
  15. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz.egg-info/SOURCES.txt +0 -0
  16. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz.egg-info/dependency_links.txt +0 -0
  17. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz.egg-info/entry_points.txt +0 -0
  18. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/src/worklog_opsdevnz.egg-info/top_level.txt +0 -0
  19. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/tests/test_cli.py +0 -0
  20. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/tests/test_config.py +0 -0
  21. {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.0}/tests/test_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: worklog-opsdevnz
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: Configurable worklog management CLI for dated development logs
5
5
  Author-email: "OpsDev.nz Collective" <john@opsdev.nz>
6
6
  License: Apache-2.0
@@ -29,6 +29,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
29
  Requires-Dist: pytest-mock>=3.10; extra == "dev"
30
30
  Requires-Dist: ruff>=0.4; extra == "dev"
31
31
  Requires-Dist: mypy>=1.0; extra == "dev"
32
+ Requires-Dist: zensical>=0.0.44; extra == "dev"
32
33
  Dynamic: license-file
33
34
 
34
35
  # worklog-opsdevnz
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "worklog-opsdevnz"
7
- version = "0.1.3"
7
+ version = "0.2.0"
8
8
  description = "Configurable worklog management CLI for dated development logs"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -37,6 +37,7 @@ dev = [
37
37
  "pytest-mock>=3.10",
38
38
  "ruff>=0.4",
39
39
  "mypy>=1.0",
40
+ "zensical>=0.0.44",
40
41
  ]
41
42
 
42
43
  [project.urls]
@@ -36,13 +36,39 @@ def generate_body(config: dict[str, Any]) -> str:
36
36
  return "\n".join(lines)
37
37
 
38
38
 
39
- def render_template(template_path: str, iso_date: str) -> str:
40
- """Render a custom Markdown template with placeholder substitution."""
39
+ def _render_tags(tags: list[str]) -> str:
40
+ """Render tags as a block-style YAML list substitution.
41
+
42
+ Empty list → '[]'. Populated list → leading newline followed by
43
+ each tag on its own indented line with '-' prefix.
44
+ """
45
+ if not tags:
46
+ return "[]"
47
+ return "\n" + "\n".join(f" - {t}" for t in tags)
48
+
49
+
50
+ def render_template(
51
+ template_path: str,
52
+ iso_date: str,
53
+ config: dict[str, Any],
54
+ ) -> str:
55
+ """Render a custom Markdown template with placeholder substitution.
56
+
57
+ Supports {{DATE}}, {{TITLE}}, {{AUTHOR}}, and {{TAGS}} placeholders.
58
+ All placeholders are case-sensitive.
59
+ """
41
60
  title = f"Work Log - {iso_date}"
61
+ tags = _render_tags(config.get("default_tags", []))
62
+ author = config.get("author", "unknown")
42
63
  with open(template_path) as f:
43
64
  content = f.read()
44
65
  content = content.replace("{{DATE}}", iso_date)
45
66
  content = content.replace("{{TITLE}}", title)
67
+ content = content.replace("{{AUTHOR}}", author)
68
+ content = content.replace("{{TAGS}}", tags)
69
+ # Clean trailing whitespace (avoids artifacts when placeholder
70
+ # substitution leaves space before a newline, e.g. 'tags: \n')
71
+ content = "\n".join(line.rstrip() for line in content.split("\n"))
46
72
  return content
47
73
 
48
74
 
@@ -52,16 +78,17 @@ def generate_content(
52
78
  ) -> str:
53
79
  """Generate full worklog content.
54
80
 
55
- If a 'template' field is set in config, renders that file as the body.
56
- Otherwise uses the built-in sections-based body.
57
- Frontmatter is always generated regardless of template.
58
- """
59
- frontmatter = generate_frontmatter(config, iso_date)
81
+ If a 'template' field is set in config, the template defines the
82
+ complete entry (including YAML frontmatter). Placeholders are
83
+ substituted but nothing else is prepended.
60
84
 
85
+ Otherwise uses the built-in default: config-driven frontmatter and
86
+ sections-based body.
87
+ """
61
88
  template_path = config.get("template")
62
89
  if template_path:
63
- body = render_template(template_path, iso_date)
64
- else:
65
- body = generate_body(config)
90
+ return render_template(template_path, iso_date, config)
66
91
 
92
+ frontmatter = generate_frontmatter(config, iso_date)
93
+ body = generate_body(config)
67
94
  return f"{frontmatter}\n{body}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: worklog-opsdevnz
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: Configurable worklog management CLI for dated development logs
5
5
  Author-email: "OpsDev.nz Collective" <john@opsdev.nz>
6
6
  License: Apache-2.0
@@ -29,6 +29,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
29
  Requires-Dist: pytest-mock>=3.10; extra == "dev"
30
30
  Requires-Dist: ruff>=0.4; extra == "dev"
31
31
  Requires-Dist: mypy>=1.0; extra == "dev"
32
+ Requires-Dist: zensical>=0.0.44; extra == "dev"
32
33
  Dynamic: license-file
33
34
 
34
35
  # worklog-opsdevnz
@@ -7,3 +7,4 @@ pytest-cov>=4.0
7
7
  pytest-mock>=3.10
8
8
  ruff>=0.4
9
9
  mypy>=1.0
10
+ zensical>=0.0.44
@@ -0,0 +1,285 @@
1
+ """Tests for frontmatter and body generation."""
2
+
3
+ import pytest
4
+
5
+ from worklog_opsdevnz.template import (
6
+ generate_frontmatter,
7
+ generate_body,
8
+ generate_content,
9
+ render_template,
10
+ )
11
+
12
+
13
+ def test_generate_frontmatter():
14
+ config = {
15
+ "author": "Test Author",
16
+ "default_tags": ["internal", "test"],
17
+ }
18
+ fm = generate_frontmatter(config, "2026-05-23")
19
+ assert "title: " in fm
20
+ assert "Work Log - 2026-05-23" in fm
21
+ assert "date: 2026-05-23" in fm
22
+ assert "author: Test Author" in fm
23
+ assert " - internal" in fm
24
+ assert " - test" in fm
25
+ assert "draft: false" in fm
26
+ assert fm.startswith("---")
27
+ assert fm.strip().endswith("---")
28
+
29
+
30
+ def test_generate_body_with_sections():
31
+ config = {
32
+ "sections": [
33
+ {"title": "Focus", "content": ""},
34
+ {"title": "Notes", "content": "some note"},
35
+ ]
36
+ }
37
+ body = generate_body(config)
38
+ assert "## Focus" in body
39
+ assert "## Notes" in body
40
+ assert "some note" in body
41
+
42
+
43
+ def test_generate_content_full():
44
+ config = {
45
+ "author": "opsdev",
46
+ "default_tags": ["log"],
47
+ "sections": [{"title": "Today", "content": ""}],
48
+ }
49
+ content = generate_content(config, "2026-05-23")
50
+ assert content.startswith("---")
51
+ assert "Work Log - 2026-05-23" in content
52
+ assert "## Today" in content
53
+
54
+
55
+ def test_render_template_with_placeholders(tmp_path):
56
+ template = tmp_path / "my-template.md"
57
+ template.write_text(
58
+ "---\n"
59
+ 'title: "{{TITLE}}"\n'
60
+ "date: {{DATE}}\n"
61
+ "author: {{AUTHOR}}\n"
62
+ "tags: {{TAGS}}\n"
63
+ "draft: false\n"
64
+ "---\n\n"
65
+ "# {{TITLE}}\n\n"
66
+ "Date: {{DATE}}\n\n"
67
+ "Free-form notes."
68
+ )
69
+
70
+ config = {"author": "opsdev", "default_tags": ["dev", "log"]}
71
+ result = render_template(str(template), "2026-05-26", config)
72
+
73
+ # Placeholders replaced
74
+ assert "{{DATE}}" not in result
75
+ assert "{{TITLE}}" not in result
76
+ assert "{{AUTHOR}}" not in result
77
+ assert "{{TAGS}}" not in result
78
+
79
+ # Content verified
80
+ assert "# Work Log - 2026-05-26" in result
81
+ assert "Date: 2026-05-26" in result
82
+ assert "Free-form notes." in result
83
+ assert 'title: "Work Log - 2026-05-26"' in result
84
+ assert "date: 2026-05-26" in result
85
+ assert "author: opsdev" in result
86
+ assert "draft: false" in result
87
+
88
+ # Tags rendered as block-style YAML
89
+ assert " - dev" in result
90
+ assert " - log" in result
91
+
92
+
93
+ def test_render_template_file_not_found():
94
+ with pytest.raises(FileNotFoundError):
95
+ render_template("/nonexistent/template.md", "2026-05-26", {})
96
+
97
+
98
+ def test_generate_content_with_template(tmp_path):
99
+ """When template is set, it defines the complete entry.
100
+
101
+ No auto-generated frontmatter is prepended.
102
+ """
103
+ template = tmp_path / "my-template.md"
104
+ template.write_text(
105
+ "---\n"
106
+ 'title: "{{TITLE}}"\n'
107
+ "date: {{DATE}}\n"
108
+ "author: {{AUTHOR}}\n"
109
+ "tags: {{TAGS}}\n"
110
+ "draft: false\n"
111
+ "---\n\n"
112
+ "# {{TITLE}}\n\n"
113
+ "Date: {{DATE}}"
114
+ )
115
+
116
+ config = {
117
+ "author": "opsdev",
118
+ "default_tags": ["log"],
119
+ "template": str(template),
120
+ }
121
+ content = generate_content(config, "2026-05-26")
122
+
123
+ # Template defines the full entry — its frontmatter is present
124
+ assert content.startswith("---")
125
+ assert 'title: "Work Log - 2026-05-26"' in content
126
+ assert "date: 2026-05-26" in content
127
+ assert "author: opsdev" in content
128
+ assert " - log" in content
129
+ assert "draft: false" in content
130
+
131
+ # Body comes from template
132
+ assert "# Work Log - 2026-05-26" in content
133
+ assert "Date: 2026-05-26" in content
134
+
135
+ # No duplicate frontmatter — only one '---' block
136
+ assert content.count("---") == 2 # opening + closing
137
+
138
+ # Sections are NOT present (template controls everything)
139
+ assert "## Focus for Today" not in content
140
+
141
+
142
+ def test_generate_content_without_template():
143
+ """No template field → built-in sections body, unchanged behaviour."""
144
+ config = {
145
+ "author": "opsdev",
146
+ "default_tags": ["log"],
147
+ "sections": [{"title": "Today", "content": ""}],
148
+ }
149
+ content = generate_content(config, "2026-05-26")
150
+ assert "## Today" in content
151
+
152
+
153
+ # ── {{TAGS}} rendering ────────────────────────────────────────────
154
+
155
+
156
+ def test_render_template_tags_empty(tmp_path):
157
+ """Empty default_tags → {{TAGS}} substitutes as '[]'."""
158
+ template = tmp_path / "t.md"
159
+ template.write_text("tags: {{TAGS}}\n")
160
+
161
+ result = render_template(str(template), "2026-06-01", {"default_tags": []})
162
+ assert "tags: []" in result
163
+ assert "{{TAGS}}" not in result
164
+
165
+
166
+ def test_render_template_tags_populated(tmp_path):
167
+ """Populated default_tags → {{TAGS}} renders as block-style YAML list."""
168
+ template = tmp_path / "t.md"
169
+ template.write_text("tags: {{TAGS}}\n")
170
+
171
+ config = {"default_tags": ["dev", "log", "ops"]}
172
+ result = render_template(str(template), "2026-06-01", config)
173
+
174
+ # Block-style: leading newline + indented items, no trailing whitespace
175
+ assert "tags:\n - dev\n - log\n - ops" in result
176
+ assert "{{TAGS}}" not in result
177
+
178
+
179
+ def test_render_template_tags_not_configured(tmp_path):
180
+ """No default_tags in config → {{TAGS}} substitutes as '[]'."""
181
+ template = tmp_path / "t.md"
182
+ template.write_text("tags: {{TAGS}}\n")
183
+
184
+ result = render_template(str(template), "2026-06-01", {})
185
+ assert "tags: []" in result
186
+
187
+
188
+ # ── {{AUTHOR}} rendering ──────────────────────────────────────────
189
+
190
+
191
+ def test_render_template_author_configured(tmp_path):
192
+ """{{AUTHOR}} pulls the author value from config."""
193
+ template = tmp_path / "t.md"
194
+ template.write_text("author: {{AUTHOR}}\n")
195
+
196
+ result = render_template(str(template), "2026-06-01", {"author": "opsdev"})
197
+ assert "author: opsdev" in result
198
+ assert "{{AUTHOR}}" not in result
199
+
200
+
201
+ def test_render_template_author_fallback(tmp_path):
202
+ """{{AUTHOR}} falls back to 'unknown' when not in config."""
203
+ template = tmp_path / "t.md"
204
+ template.write_text("author: {{AUTHOR}}\n")
205
+
206
+ result = render_template(str(template), "2026-06-01", {})
207
+ assert "author: unknown" in result
208
+
209
+
210
+ # ── Full-entry template edge cases ────────────────────────────────
211
+
212
+
213
+ def test_generate_content_template_defines_full_entry(tmp_path):
214
+ """Template with custom YAML fields — the tool only substitutes placeholders."""
215
+ template = tmp_path / "custom.md"
216
+ template.write_text(
217
+ "---\n"
218
+ 'title: "{{TITLE}}"\n'
219
+ "date: {{DATE}}\n"
220
+ "author: {{AUTHOR}}\n"
221
+ "tags: {{TAGS}}\n"
222
+ "mood: creative\n"
223
+ "project: outcome-engineering\n"
224
+ "draft: false\n"
225
+ "---\n\n"
226
+ "# {{TITLE}}\n\n"
227
+ "Custom body content."
228
+ )
229
+
230
+ config = {
231
+ "author": "opsdev",
232
+ "default_tags": ["dev"],
233
+ "template": str(template),
234
+ }
235
+ content = generate_content(config, "2026-06-05")
236
+
237
+ # Custom fields preserved verbatim
238
+ assert "mood: creative" in content
239
+ assert "project: outcome-engineering" in content
240
+
241
+ # Placeholders substituted
242
+ assert 'title: "Work Log - 2026-06-05"' in content
243
+ assert "date: 2026-06-05" in content
244
+ assert "author: opsdev" in content
245
+ assert " - dev" in content
246
+
247
+ # Body preserved
248
+ assert "Custom body content." in content
249
+
250
+ # No auto-generated frontmatter prepended
251
+ assert content.count("---") == 2 # only the template's opening/closing
252
+
253
+
254
+ def test_generate_content_template_no_frontmatter(tmp_path):
255
+ """Template without frontmatter — entry has no frontmatter at all."""
256
+ template = tmp_path / "body-only.md"
257
+ template.write_text("# {{TITLE}}\n\nJust body content.")
258
+
259
+ config = {"template": str(template)}
260
+ content = generate_content(config, "2026-06-05")
261
+
262
+ # No frontmatter markers, just the rendered template
263
+ assert "---" not in content
264
+ assert "# Work Log - 2026-06-05" in content
265
+ assert "Just body content." in content
266
+
267
+
268
+ def test_render_template_case_sensitive(tmp_path):
269
+ """Lowercase placeholders are NOT substituted."""
270
+ template = tmp_path / "t.md"
271
+ template.write_text(
272
+ "title: {{title}}\n"
273
+ "date: {{date}}\n"
274
+ "author: {{author}}\n"
275
+ "tags: {{tags}}\n"
276
+ )
277
+
278
+ config = {"author": "opsdev", "default_tags": ["dev"]}
279
+ result = render_template(str(template), "2026-06-05", config)
280
+
281
+ # Lowercase placeholders left untouched
282
+ assert "{{title}}" in result
283
+ assert "{{date}}" in result
284
+ assert "{{author}}" in result
285
+ assert "{{tags}}" in result
@@ -1,99 +0,0 @@
1
- """Tests for frontmatter and body generation."""
2
-
3
- import pytest
4
-
5
- from worklog_opsdevnz.template import (
6
- generate_frontmatter,
7
- generate_body,
8
- generate_content,
9
- render_template,
10
- )
11
-
12
-
13
- def test_generate_frontmatter():
14
- config = {
15
- "author": "Test Author",
16
- "default_tags": ["internal", "test"],
17
- }
18
- fm = generate_frontmatter(config, "2026-05-23")
19
- assert "title: " in fm
20
- assert "Work Log - 2026-05-23" in fm
21
- assert "date: 2026-05-23" in fm
22
- assert "author: Test Author" in fm
23
- assert " - internal" in fm
24
- assert " - test" in fm
25
- assert "draft: false" in fm
26
- assert fm.startswith("---")
27
- assert fm.strip().endswith("---")
28
-
29
-
30
- def test_generate_body_with_sections():
31
- config = {
32
- "sections": [
33
- {"title": "Focus", "content": ""},
34
- {"title": "Notes", "content": "some note"},
35
- ]
36
- }
37
- body = generate_body(config)
38
- assert "## Focus" in body
39
- assert "## Notes" in body
40
- assert "some note" in body
41
-
42
-
43
- def test_generate_content_full():
44
- config = {
45
- "author": "opsdev",
46
- "default_tags": ["log"],
47
- "sections": [{"title": "Today", "content": ""}],
48
- }
49
- content = generate_content(config, "2026-05-23")
50
- assert content.startswith("---")
51
- assert "Work Log - 2026-05-23" in content
52
- assert "## Today" in content
53
-
54
-
55
- def test_render_template_with_placeholders(tmp_path):
56
- template = tmp_path / "my-template.md"
57
- template.write_text("# {{TITLE}}\n\nDate: {{DATE}}\n\nFree-form notes.")
58
-
59
- result = render_template(str(template), "2026-05-26")
60
- assert "# Work Log - 2026-05-26" in result
61
- assert "Date: 2026-05-26" in result
62
- assert "Free-form notes." in result
63
- assert "{{DATE}}" not in result
64
- assert "{{TITLE}}" not in result
65
-
66
-
67
- def test_render_template_file_not_found():
68
- with pytest.raises(FileNotFoundError):
69
- render_template("/nonexistent/template.md", "2026-05-26")
70
-
71
-
72
- def test_generate_content_with_template(tmp_path):
73
- template = tmp_path / "my-template.md"
74
- template.write_text("# {{TITLE}}\n\nDate: {{DATE}}")
75
-
76
- config = {
77
- "author": "opsdev",
78
- "default_tags": ["log"],
79
- "template": str(template),
80
- }
81
- content = generate_content(config, "2026-05-26")
82
- assert content.startswith("---")
83
- assert "Work Log - 2026-05-26" in content
84
- assert "author: opsdev" in content
85
- assert "# Work Log - 2026-05-26" in content
86
- assert "Date: 2026-05-26" in content
87
- # Frontmatter generated, body from template — not sections
88
- assert "## " not in content.split("---\n")[2] if "---\n" in content else True
89
-
90
-
91
- def test_generate_content_without_template():
92
- """No template field → built-in sections body, unchanged behaviour."""
93
- config = {
94
- "author": "opsdev",
95
- "default_tags": ["log"],
96
- "sections": [{"title": "Today", "content": ""}],
97
- }
98
- content = generate_content(config, "2026-05-26")
99
- assert "## Today" in content