render-engine 2026.1.1a1__py3-none-any.whl → 2026.1.1a2__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.
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2026.1.1a1'
32
- __version_tuple__ = version_tuple = (2026, 1, 1, 'a1')
31
+ __version__ = version = '2026.1.1a2'
32
+ __version_tuple__ = version_tuple = (2026, 1, 1, 'a2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -12,10 +12,9 @@ from more_itertools import batched
12
12
  from render_engine_parser import BasePageParser
13
13
  from slugify import slugify
14
14
 
15
- from render_engine.content_managers import ContentManager, FileContentManager
16
-
17
15
  from ._base_object import BaseObject
18
16
  from .archive import Archive
17
+ from .content_managers import ContentManager, FileContentManager
19
18
  from .feeds import RSSFeed
20
19
  from .page import Page
21
20
  from .plugins import PluginManager
@@ -247,6 +246,9 @@ class Collection(BaseObject):
247
246
  def __iter__(self):
248
247
  yield from self.content_manager
249
248
 
249
+ def __len__(self):
250
+ return len(self.content_manager)
251
+
250
252
  @property
251
253
  def all_content(self) -> Generator:
252
254
  """All of the content that is associated with the Collection including Pages, Feed, and Archives"""
@@ -292,8 +294,7 @@ class Collection(BaseObject):
292
294
  entry.plugin_manager = copy.deepcopy(self.plugin_manager)
293
295
 
294
296
  entry.site = self.site
295
- for route in entry.routes:
296
- entry.render(route, self.site.theme_manager)
297
+ entry.render(self.site.theme_manager)
297
298
 
298
299
  def render(self) -> None:
299
300
  """Iterate through Pages and Check for Archives and Feeds"""
@@ -19,4 +19,21 @@ class ContentManager(ABC):
19
19
  @abstractmethod
20
20
  def create_entry(self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None):
21
21
  """Create a new entry"""
22
- ...
22
+ pass
23
+
24
+ def find_entry(self, **kwargs):
25
+ """
26
+ Find an entry
27
+
28
+ :param kwargs: List of attributes to search by
29
+ :return: Page if it was found otherwise None
30
+ """
31
+ for page in self:
32
+ if all(getattr(page, attr, None) == value for attr, value in kwargs.items()):
33
+ return page
34
+ return None
35
+
36
+ @abstractmethod
37
+ def update_entry(self, *, page, **kwargs):
38
+ """Update an entry"""
39
+ pass
@@ -36,8 +36,16 @@ class FileContentManager(ContentManager):
36
36
  def pages(self, value: Iterable):
37
37
  self._pages = value
38
38
 
39
+ def __len__(self):
40
+ return len(list(self.pages))
41
+
39
42
  def create_entry(
40
- self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None
43
+ self,
44
+ filepath: Path = None,
45
+ editor: str = None,
46
+ metadata: dict = None,
47
+ content: str = None,
48
+ update: bool = False,
41
49
  ) -> str:
42
50
  """
43
51
  Create a new entry for the Collection
@@ -46,12 +54,33 @@ class FileContentManager(ContentManager):
46
54
  :param editor: Editor to open to edit the entry.
47
55
  :param content: The content for the entry
48
56
  :param metadata: Metadata for the new entry
57
+ :param update: Allow overwriting the existing file
49
58
  """
50
59
  if not filepath:
51
60
  raise ValueError("filepath needs to be specified.")
52
61
 
62
+ if not update and filepath.exists():
63
+ raise RuntimeError(f"File at {filepath} exists and update is disabled.")
64
+
53
65
  parsed_content = self.collection.Parser.create_entry(content=content, **metadata)
54
66
  filepath.write_text(parsed_content)
55
67
  if editor:
56
68
  subprocess.run([editor, filepath])
57
69
  return f"New entry created at {filepath} ."
70
+
71
+ def update_entry(self, page, *, content: str = None, **kwargs) -> str:
72
+ """
73
+ Update an entry
74
+
75
+ :param page: Page object to update
76
+ :param content: Content for the updated page
77
+ :param kwargs: Attributes to be included in the updated page
78
+ :return: String indicating that the page was updated.
79
+ """
80
+ self.create_entry(filepath=page.content_path, metadata=kwargs, content=content, update=True)
81
+ if self._pages:
82
+ self._pages = [
83
+ existing_page for existing_page in self._pages if page.content_path != existing_page.content_path
84
+ ]
85
+ self._pages.append(self.collection.get_page(page.content_path))
86
+ return f"Entry at {page.content_path} updated."
render_engine/page.py CHANGED
@@ -149,20 +149,23 @@ class BasePage(BaseObject):
149
149
  def __repr__(self) -> str:
150
150
  return f"<Page: {self._title}>"
151
151
 
152
- def render(self, route: str | Path, theme_manager: ThemeManager) -> int:
152
+ def render(self, theme_manager: ThemeManager) -> int:
153
153
  """Render the page to the file system"""
154
- path = Path(self.site.output_path) / Path(route) / Path(self.path_name)
155
- path.parent.mkdir(parents=True, exist_ok=True)
156
- settings = dict()
157
- if (pm := getattr(self, "plugin_manager", None)) and pm is not None:
158
- settings = {**self.site.plugin_manager.plugin_settings, "route": route}
159
- pm.hook.render_content(page=self, settings=settings, site=self.site)
160
- self.rendered_content = self._render_content(theme_manager.engine)
161
- # pass the route to the plugin settings
162
- if pm is not None:
163
- pm.hook.post_render_content(page=self.__class__, settings=settings, site=self.site)
164
-
165
- return path.write_text(self.rendered_content)
154
+ rc = 0
155
+ for route in self.routes:
156
+ path = Path(self.site.output_path) / Path(route) / Path(self.path_name)
157
+ path.parent.mkdir(parents=True, exist_ok=True)
158
+ settings = dict()
159
+ if (pm := getattr(self, "plugin_manager", None)) and pm is not None:
160
+ settings = {**self.site.plugin_manager.plugin_settings, "route": route}
161
+ pm.hook.render_content(page=self, settings=settings, site=self.site)
162
+ self.rendered_content = self._render_content(theme_manager.engine)
163
+ # pass the route to the plugin settings
164
+ if pm is not None:
165
+ pm.hook.post_render_content(page=self.__class__, settings=settings, site=self.site)
166
+
167
+ rc += path.write_text(self.rendered_content)
168
+ return rc
166
169
 
167
170
 
168
171
  class Page(BasePage):
@@ -242,6 +245,8 @@ class Page(BasePage):
242
245
  for key, val in self.metadata.items():
243
246
  setattr(self, key.lower(), val)
244
247
 
248
+ self.content_path = content_path
249
+
245
250
  @property
246
251
  def _content(self) -> Any:
247
252
  """
render_engine/site.py CHANGED
@@ -306,14 +306,11 @@ class Site:
306
306
  args = []
307
307
  match entry:
308
308
  case Page():
309
- for route in entry.routes:
310
- progress.update(
311
- task_add_route,
312
- description=f"[blue]Adding[gold]Route: [blue]{entry._slug}",
313
- )
314
-
315
- # self._render_output(route, entry)
316
- args = [route, self.theme_manager]
309
+ progress.update(
310
+ task_add_route,
311
+ description=f"[blue]Adding[gold]Route: [blue]{entry._slug}",
312
+ )
313
+ args = [self.theme_manager]
317
314
  case Collection():
318
315
  progress.update(
319
316
  task_add_route,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: render_engine
3
- Version: 2026.1.1a1
3
+ Version: 2026.1.1a2
4
4
  Summary: A Flexible Static Site Generator for Python
5
5
  Project-URL: homepage, https://github.com/render-engine/render-engine/
6
6
  Project-URL: repository, https://github.com/render-engine/render-engine/
@@ -1,24 +1,24 @@
1
1
  render_engine/.gitignore,sha256=74oa8YR8gNxKxB6lCtoqmgtB2xpZnWM539Qsl4wI_lg,12
2
2
  render_engine/__init__.py,sha256=3fgua4ZA9o1pvQ5unhY1gRARLXFqAu019NEYqZTjP20,154
3
3
  render_engine/__main__.py,sha256=uI7aBBZz0qSDwwwD11nS5oltWsuLw9hStfYo8O1aNws,144
4
- render_engine/__version__.py,sha256=hfZmZGRgu1zYwAQDJZKkcPJnWnfiuAJLIE-fSAXX7c8,718
4
+ render_engine/__version__.py,sha256=pI2d9qsCwWR0-5U4R9QwGAWwJN7Kas0dzZtgcWUBLBU,718
5
5
  render_engine/_base_object.py,sha256=B05rDWJUe5GM5FF94NeYY71KweBHY-IYel1lSBwLOrU,3574
6
6
  render_engine/archive.py,sha256=S3-kCmDNVKkEfKDKxcEk-sXkBD0vS0RDnFfPunYkU8g,2072
7
7
  render_engine/blog.py,sha256=f9GqFUFsta0KZnFhCiajobpfQyALqvgI5sbLm6zt1zw,1571
8
- render_engine/collection.py,sha256=9x2ixfxRPc2es3vVuCeJbvncLxz3-BFvg2Kj3RCFHk0,12184
8
+ render_engine/collection.py,sha256=63KL7kAyJYsS8isbugqT6Ii-CUTxj5GST-f7yZJzZaA,12189
9
9
  render_engine/engine.py,sha256=GOtUiq4ny5GHaLSCeH5u1Zk1JnWJVh63vK7etJiwS20,2843
10
10
  render_engine/feeds.py,sha256=i-VHsb6pRplMzaenBn6oeqh9yI_N4WVUAExPox6iJgw,921
11
11
  render_engine/hookspecs.py,sha256=GhOpw0zTQjfwWOFYYbJ4P7Cvq-oy1MmTPHmd90dr0kg,2292
12
12
  render_engine/links.py,sha256=pKmQMTz8-yGX8IecHcrlF3Dkejk7cptaO3qCkQiHB9I,2560
13
- render_engine/page.py,sha256=FqyjgVdeNwYx3uoWSS-YqxgN_0KFRQkN46mgSKDwJdQ,9020
13
+ render_engine/page.py,sha256=o753fqux-VMOHmprp6RFZOHqRt8Gjtc32WX3h5WLohA,9153
14
14
  render_engine/plugins.py,sha256=NXM8QTbbRV-DwgpQRoIhILijJBN4SyYg2Rkk1LUAuZM,4703
15
15
  render_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- render_engine/site.py,sha256=aarHkveSVDOy7hRPzrx_ga3DmmirZQcLvcJdyw-b8lY,13329
16
+ render_engine/site.py,sha256=22ir1dc2qWpmfRyG2hmTzB8BInZkp37LSaRo97xufto,13186
17
17
  render_engine/site_map.py,sha256=I1p_yMDMy1jpIivgNgZptnsxZa8NkcrpciVJE3DlFlQ,5422
18
18
  render_engine/themes.py,sha256=TFG1rd34QCBvBWfeDbawgsn6kprmjsDTa1pdDSwDMic,4207
19
19
  render_engine/content_managers/__init__.py,sha256=z1x99J0GNcfqYFrugD0EleiZR6b-sfM6zViDTH1iF0s,161
20
- render_engine/content_managers/base_content_manager.py,sha256=umb2Tze7UE7ON74hOe_WiykOk9-xTPw40_l3W5Cy5_U,620
21
- render_engine/content_managers/file_content_manager.py,sha256=pKyldWKGocI2WAY8H_6d9dWY26hUxm4N5_oavO_FxJ0,1884
20
+ render_engine/content_managers/base_content_manager.py,sha256=9DiQFN461jfdmcQF_0rQwgdNcUzaMDQaLFVbbiZf2e8,1088
21
+ render_engine/content_managers/file_content_manager.py,sha256=kD5D99E16pN6PobDo8isqmPYdJyoRSZo9BP9iZaBwRI,2963
22
22
  render_engine/extras/__init__.py,sha256=L4jr4A7Jl-ODnSx1q2fP3_dBo37Dw6yepNRddu1nFNo,72
23
23
  render_engine/parsers/markdown.py,sha256=0jpixCaoHaL0IRSvFIljJIRCvFkXoKTEYQNK38LwMDU,287
24
24
  render_engine/render_engine_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -36,7 +36,7 @@ render_engine/render_engine_templates/base_templates/_page.html,sha256=jjrY2BAwl
36
36
  render_engine/render_engine_templates/components/footer.html,sha256=HkPGGhfN0HcYm7t8zgXWCQ3bsCbT8FxT4_n2-9e1zUE,74
37
37
  render_engine/render_engine_templates/components/page_title.html,sha256=l8aE1TY94UPHXHqAyy6jv4IoN2Hv9cbrTPh7ILkMyxg,137
38
38
  render_engine/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- render_engine-2026.1.1a1.dist-info/METADATA,sha256=ElQwTlE7OJj5tzl3SxQqemrblppdtOQs1PXrVXCw-4M,11894
40
- render_engine-2026.1.1a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- render_engine-2026.1.1a1.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
42
- render_engine-2026.1.1a1.dist-info/RECORD,,
39
+ render_engine-2026.1.1a2.dist-info/METADATA,sha256=FfVYaHJMWBGPe6UDAV97e7y7YHfKLL9fR1KUl1urc8w,11894
40
+ render_engine-2026.1.1a2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ render_engine-2026.1.1a2.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
42
+ render_engine-2026.1.1a2.dist-info/RECORD,,