ssb-pubmd 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -0,0 +1,46 @@
1
+ from ssb_pubmd.adapters.content_parser import ContentParser
2
+ from ssb_pubmd.adapters.document_processor import DocumentProcessor
3
+ from ssb_pubmd.adapters.publish_client import PublishClient
4
+ from ssb_pubmd.adapters.storage import Storage
5
+
6
+ USER_KEY_PREFIX = "user:"
7
+ DOCUMENT_KEY = "app:document"
8
+
9
+ def sync_document(
10
+ raw_document_content: str,
11
+ document_processor: DocumentProcessor,
12
+ content_parser: ContentParser,
13
+ storage: Storage,
14
+ publish_client: PublishClient,
15
+ ) -> str:
16
+ document_processor.load(raw_document_content)
17
+
18
+ document_metadata = document_processor.extract_metadata(target_key="ssb")
19
+ document_metadata["content_type"] = "article"
20
+
21
+ document_publish_path = storage.get(DOCUMENT_KEY).get("publish_path")
22
+ if not document_publish_path:
23
+ content = content_parser.parse(metadata=document_metadata, html=None)
24
+ response = publish_client.send_content(content)
25
+ storage.update(
26
+ DOCUMENT_KEY,
27
+ {"publish_id": response.publish_id, "publish_path": response.publish_path},
28
+ )
29
+ document_publish_path = response.publish_path
30
+
31
+ document_elements = document_processor.extract_elements(target_class="ssb")
32
+ for id_, html in document_elements:
33
+ key = USER_KEY_PREFIX + id_
34
+ metadata = storage.get(key) | {"publish_folder": document_publish_path}
35
+ component = content_parser.parse(metadata, html)
36
+ response = publish_client.send_content(component)
37
+ storage.update(key, {"publish_id": response.publish_id})
38
+ document_processor.replace_element(id_, response.publish_html)
39
+
40
+ article_metadata = document_metadata | {
41
+ "publish_id": storage.get(DOCUMENT_KEY).get("publish_id")
42
+ }
43
+ html = document_processor.extract_html()
44
+ article = content_parser.parse(metadata=article_metadata, html=html)
45
+ response = publish_client.send_content(article)
46
+ return response.publish_url
@@ -0,0 +1,133 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ import narwhals as nw
5
+ from narwhals.typing import IntoDataFrame
6
+
7
+ from ssb_pubmd.adapters.content_parser import Content
8
+ from ssb_pubmd.adapters.content_parser import ContentParser
9
+ from ssb_pubmd.adapters.content_parser import MimirContentParser
10
+ from ssb_pubmd.adapters.storage import LocalFileStorage
11
+ from ssb_pubmd.adapters.storage import Storage
12
+ from ssb_pubmd.domain.document_publisher import USER_KEY_PREFIX
13
+
14
+ STORAGE: Storage = LocalFileStorage(project_folder=Path.cwd())
15
+ CONTENT_PARSER: ContentParser = MimirContentParser()
16
+
17
+ class NotebookClientError(Exception): ...
18
+
19
+ def configure_factbox(
20
+ key: str,
21
+ title: str,
22
+ display_type: Literal["default", "sneakPeek", "aiIcon"] = "default",
23
+ ) -> None:
24
+ """Oppretter en faktaboks og printer en Markdown-snippet som kan limes inn i artikkelen (på en ny linje).
25
+
26
+ :param key: En unik nøkkel for innholdet.
27
+ :param title: Tittelen til faktaboksen.
28
+ :param display_type: Visning av faktaboksen.
29
+
30
+ Alternativer:
31
+
32
+ * "default": Bare tittel (standard)
33
+ * "sneakPeek": Tittel og litt av forklaringsteksten
34
+ * "aiIcon": Tittel og litt av forklaringsteksten + KI-ikon
35
+ """
36
+ metadata = {
37
+ "content_type": "factBox",
38
+ "title": title,
39
+ "display_type": display_type,
40
+ }
41
+
42
+ content = CONTENT_PARSER.parse(
43
+ metadata=metadata,
44
+ html=None,
45
+ )
46
+ _store_user_content(user_key=key, content=content)
47
+
48
+ md = _get_markdown_snippet(
49
+ key,
50
+ placeholder_text="Faktaboksens tekst skrives her med Markdown-syntaks.\n\n### Underoverskrift\n\nNy paragraf.",
51
+ )
52
+ print(md)
53
+
54
+
55
+ def create_highchart(
56
+ key: str,
57
+ title: str,
58
+ dataframe: IntoDataFrame | None = None,
59
+ tbml: str | None = None,
60
+ graph_type: Literal["line", "pie", "column", "bar", "area", "barNegative"] = "line",
61
+ xlabel: str = "x",
62
+ ylabel: str = "y"
63
+ ) -> None:
64
+ """Oppretter et highchart og printer en Markdown-snippet som kan limes inn i artikkelen (på en ny linje).
65
+
66
+ Som datakilde er det nødvendig å spesifisere enten `dataframe` eller `tbml`.
67
+
68
+ :param key: En unik nøkkel for innholdet.
69
+ :param title: Tittelen til highchartet.
70
+ :param dataframe: En pandas, Polars eller PyArrow dataframe.
71
+ :param tbml: URL eller TBML-id.
72
+ :param graph_type: Graftype.
73
+
74
+ Alternativer:
75
+
76
+ * "line": Linje (standard)
77
+ * "pie": Kake
78
+ * "column": Stolpe
79
+ * "bar": Liggende stolpe
80
+ * "area": Areal
81
+ * "barNegative": Pyramide
82
+
83
+ :param xlabel: X-akse, tittel.
84
+ :param ylabel: Y-akse, tittel.
85
+ """
86
+ if dataframe is None and tbml is None:
87
+ raise NotebookClientError("Either 'dataframe' or 'tbml' must be specified.")
88
+ metadata = {
89
+ "content_type": "highchart",
90
+ "title": title,
91
+ "graph_type": graph_type,
92
+ "xlabel": xlabel,
93
+ "ylabel": ylabel
94
+ }
95
+ if tbml is not None:
96
+ metadata["tbml"] = tbml
97
+ html = _dataframe_to_html_table(dataframe) if dataframe is not None else None
98
+ content = CONTENT_PARSER.parse(metadata, html)
99
+ _store_user_content(user_key=key, content=content)
100
+
101
+ md = _get_markdown_snippet(key)
102
+ print(md)
103
+
104
+
105
+ def _store_user_content(user_key: str, content: Content) -> None:
106
+ key = USER_KEY_PREFIX + str(user_key)
107
+ STORAGE.update(key, content.to_dict())
108
+
109
+
110
+ def _dataframe_to_html_table(dataframe: IntoDataFrame) -> str:
111
+ df = nw.from_native(dataframe)
112
+ html = "<table><tbody>\n"
113
+
114
+ html += "<tr>\n"
115
+ for name in df.columns:
116
+ html += f" <td>{name}</td>\n"
117
+ html += "</tr>\n"
118
+
119
+ for row in df.iter_rows():
120
+ html += " <tr>\n"
121
+ for value in row:
122
+ html += f" <td>{value}</td>\n"
123
+ html += " </tr>\n"
124
+
125
+ html += "</tbody></table>"
126
+
127
+ return html
128
+
129
+
130
+ def _get_markdown_snippet(key: str, placeholder_text: str | None = None) -> str:
131
+ div_config = f"{{ #{key} .ssb }}"
132
+ div_content = f"\n{placeholder_text}\n\n" if placeholder_text is not None else ""
133
+ return f"::: {div_config}\n{div_content}:::"
File without changes
@@ -0,0 +1,138 @@
1
+ ---
2
+ ssb:
3
+ path: /utdanning/grunnskoler/statistikk/elevar-i-grunnskolen/artikler
4
+ title: "Hovedtittel for artikkelen"
5
+ authors:
6
+ - name: Fornavn Etternavn
7
+ email: ---@ssb.no
8
+ - name: Fornavn Etternavn
9
+ email: ---@ssb.no
10
+ ingress: |
11
+ Ingress for artikkelen,
12
+ som kan skrives over flere linjer.
13
+ ---
14
+ Dette er en SSB-artikkel skrevet med Quarto Markdown.
15
+
16
+ Neste steg:
17
+
18
+ 1. Sjekk at du er logget inn i CMS-et: {{ cms_admin_url }}/tool/com.enonic.app.contentstudio/main.
19
+
20
+ 2. Det er gitt et eksempel på CMS-mappe i feltet `path` ovenfor. Endre denne til CMS-mappen du ønsker å opprette artikkelen i. Dette må være en mappe du har skrivetilgang til.
21
+
22
+ 3. Kjør kommandoen `poetry run ssb-pubmd preview {{ filepath }}`.
23
+
24
+ * Dette vil opprette en draft-artikkel og printe URL'en til en forhåndsvisning av artikkelen.
25
+ * Kommandoen fungerer tilsvarende som `quarto preview`, det vil si at forhåndsvisningen holdes oppdatert etter hvert som du endrer og lagrer denne filen.****
26
+ * Du må oppdatere nettleservinduet for å se endringene.
27
+
28
+ 4. Les seksjonene nedenfor for en kort introduksjon til skriving av SSB-artikler i Quarto Markdown.
29
+
30
+ ## Artikkelmetadata
31
+
32
+ Metadata-blokken øverst viser all metadata som kan settes for en artikkel.
33
+ Dette er noe begrenset i testversjonen, men vil senere være komplett.
34
+ Merk at `path` og `title` er obligatoriske felt.
35
+
36
+
37
+ ## Markdown
38
+
39
+ Se [Quarto-referansen](https://quarto.org/docs/authoring/markdown-basics.html). Merk at:
40
+
41
+ * Hovedoverskrift (`#`) støttes ikke, og skal settes i metadatablokken.
42
+
43
+ ## Kode
44
+
45
+ Se [Quarto-referansen](https://quarto.org/docs/computations/python.html). Merk at:
46
+
47
+ * Ingen kodeblokker eller output vil komme med i artikkelen.
48
+ * [Kode i teksten](https://quarto.org/docs/computations/inline-code.html) støttes. Koden vil evalueres og erstattes med tekst før artikkelen sendes til CMS'et.
49
+ * All kode må kunne kjøres fra topp til bunn.
50
+
51
+ ## SSB-komponenter
52
+
53
+ I testversjonen tilbys et begrenset utvalg SSB-komponenter.
54
+ Komponentene opprettes med Python og settes inn med Markdown.
55
+
56
+ ### Opprette komponenter
57
+
58
+ Importér Python-pakken:
59
+
60
+ ```{python}
61
+ import ssb_pubmd as ssb
62
+ ```
63
+
64
+ Kodeblokkene nedenfor viser opprettelse av to komponenter; et highchart og en faktaboks:
65
+ ```{python}
66
+ import pandas as pd
67
+ data = {
68
+ "År": [
69
+ 2000, 2001, 2002, 2003, 2004,
70
+ 2005, 2006, 2007, 2008, 2009,
71
+ 2010, 2011, 2012, 2013, 2014,
72
+ 2015, 2016, 2017, 2018, 2019
73
+ ],
74
+ "Andel": [
75
+ 4, 3.3, 4.7, 5.6, 7.2,
76
+ 6.7, 7, 7.3, 7.6, 7.7,
77
+ 7.7, 7.6, 8, 8.6, 9.4,
78
+ 10, 10.3, 10.7, 11.3, 11.7
79
+ ]
80
+ }
81
+ df = pd.DataFrame(data)
82
+
83
+ ssb.Highchart(
84
+ key="my-highchart",
85
+ title="Highchart title",
86
+ dataframe=df,
87
+ graph_type="line",
88
+ xlabel="my_xlabel",
89
+ ylabel="my_ylabel"
90
+ )
91
+ ```
92
+
93
+ ```{python}
94
+ ssb.Factbox(
95
+ key = "my-factbox",
96
+ title = "Factbox title",
97
+ display_type = "default")
98
+ ```
99
+
100
+ Dokumentasjon: (link kommer).
101
+ * For å se alle funksjonene som tilbys, skriv `ssb.` på en kodelinje og tast `Ctrl + Space` (VSCode) eller `Tab` (Jupyterlab).
102
+ * VSCode: Dokumentasjonen til funksjonen vises automatisk i en popup-boks når man hovrer over funksjonen.
103
+
104
+ ### Sette inn komponenter
105
+
106
+ Når du kjører en av kodeblokkene ovenfor, vil det printes en Markdown-snippet som kan kopieres og limes inn på en ny linje:
107
+
108
+ ::: { #my-highchart .ssb }
109
+ :::
110
+
111
+ Merk at faktabokser settes inn med innhold:
112
+
113
+ ::: { #my-factbox .ssb }
114
+
115
+ Faktaboksens tekst skrives her med Markdown-syntaks.
116
+
117
+ ### Underoverskrift
118
+
119
+ Ny paragraf.
120
+
121
+ :::
122
+
123
+
124
+ ## Synkronisering
125
+
126
+ Etter å ha synkronisert denne filen til CMS'et, vil du se en ekstra fil i Dapla-tjenesten:
127
+
128
+ ```
129
+ {{ project_folder }}/
130
+ ├── {{ filename }}
131
+ ├── .ssbno.json
132
+ ```
133
+
134
+ Metadatafilen `.ssbno.json` er automatisk generert og inneholder metadata for den tilhørende CMS-artikkelen. Merk at:
135
+
136
+ * Dersom `.ssbno.json` slettes, vil en ny CMS-artikkel opprettes ved neste synkronisering.
137
+ * Dersom man jobber med to separate SSB-artikler, skal disse ligge i hver sin mappe, slik at de har hver sin metadatafil.
138
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ssb-pubmd
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: SSB Pubmd
5
5
  License: MIT
6
6
  Author: Olav Landsverk
@@ -13,12 +13,15 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
- Requires-Dist: cryptography (>=45.0.5,<46.0.0)
17
- Requires-Dist: google-cloud-secret-manager (>=2.24.0,<3.0.0)
16
+ Requires-Dist: dapla-auth-client (>=1.2.5,<2.0.0)
17
+ Requires-Dist: ipynbname (>=2025.8.0.0,<2026.0.0.0)
18
+ Requires-Dist: narwhals (>=2.15.0,<3.0.0)
18
19
  Requires-Dist: nbformat (>=5.10.4,<6.0.0)
19
- Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
20
+ Requires-Dist: nh3 (>=0.3.2,<0.4.0)
21
+ Requires-Dist: pandocfilters (>=1.5.1,<2.0.0)
22
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
20
23
  Requires-Dist: requests (>=2.32.4,<3.0.0)
21
- Requires-Dist: types-requests (>=2.32.4.20250611,<3.0.0.0)
24
+ Requires-Dist: watchfiles (>=1.1.1,<2.0.0)
22
25
  Project-URL: Changelog, https://github.com/statisticsnorway/ssb-pubmd/releases
23
26
  Project-URL: Documentation, https://statisticsnorway.github.io/ssb-pubmd
24
27
  Project-URL: Homepage, https://github.com/statisticsnorway/ssb-pubmd
@@ -0,0 +1,18 @@
1
+ ssb_pubmd/__init__.py,sha256=GmZebzEEIJcwwYD6_J5irpY5-lGAKFr8lRrTGB_nAPA,170
2
+ ssb_pubmd/__main__.py,sha256=7Trn-DZkNbVn6s5J8FVg9JFtpFEHv-4VKZ34MVs23Cc,204
3
+ ssb_pubmd/adapters/content_parser.py,sha256=ExOULfwoFBVb9h2wH8m61fWFy192ZEIG2LI3fuFRpbE,5141
4
+ ssb_pubmd/adapters/document_processor.py,sha256=GN4FJmWcyiCdTBzWkyUReFGd7AME-F4Eq5N4JmarddQ,4271
5
+ ssb_pubmd/adapters/publish_client.py,sha256=5JIVtJCV_uLDv6YwGitQLE_T4dLWaWQCm9nO6F9RpR4,3350
6
+ ssb_pubmd/adapters/storage.py,sha256=Dexfgw0csQ9wljC6lqf9kFmoM2CHdfMghm-qBrgdWjM,1227
7
+ ssb_pubmd/cli.py,sha256=fOJKHz8l871k5rS-1b1IUzgnhJozMhiujXwzqyeL6og,3262
8
+ ssb_pubmd/config.py,sha256=FbHkHDobBdqZLT9ZZKfH9Ch0wtj4TmKNF8EMCbQAJgE,844
9
+ ssb_pubmd/domain/document_publisher.py,sha256=hgzJx9kGZJOVLrgFRg-JGAmNsasvHA9_BA1g3htcWrQ,1920
10
+ ssb_pubmd/notebook_client.py,sha256=w4Jlo6CH5tVTzSxZlD0bPn0CPwgdX2NL7nKzlfstheU,4090
11
+ ssb_pubmd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ ssb_pubmd/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ ssb_pubmd/templates/template.qmd,sha256=s4aEMlseEyZMytHrNz8HpcYd1JG2UPwTOL_edJ48qmQ,4084
14
+ ssb_pubmd-0.1.2.dist-info/LICENSE,sha256=tF5bnYv09fgH5ph9t1EpH1MGrVOGTQeswL4dzVeZ_ak,1073
15
+ ssb_pubmd-0.1.2.dist-info/METADATA,sha256=NsDUsihthkrVF4tv7wXzp4e6JTD6gvOx_nbim_w1PLk,4101
16
+ ssb_pubmd-0.1.2.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
17
+ ssb_pubmd-0.1.2.dist-info/entry_points.txt,sha256=o4oU99zbZNIBKGYWdgdEG6ev-62ZRWEJOe7EOjJaajk,53
18
+ ssb_pubmd-0.1.2.dist-info/RECORD,,
ssb_pubmd/adapters/cli.py DELETED
@@ -1,21 +0,0 @@
1
- import sys
2
- from dataclasses import dataclass
3
-
4
- from ssb_pubmd.ports import CmsManager
5
-
6
-
7
- @dataclass
8
- class CliAdapter:
9
- cms_manager: CmsManager
10
-
11
- def run(self, system_arguments: list[str]) -> None:
12
- match system_arguments:
13
- case [_, "sync", file_path]:
14
- preview = self.cms_manager.sync(file_path)
15
- print(f"Preview URL: {preview}")
16
- # except Exception as e:
17
- # print(f"Error during sync: {e}")
18
- # sys.exit(1)
19
- case _:
20
- print("Usage: ssb-pubmd sync <content_file_path>")
21
- sys.exit(1)
@@ -1,47 +0,0 @@
1
- from urllib.parse import urlparse
2
-
3
- import requests
4
-
5
- from ssb_pubmd.models import CmsResponse
6
- from ssb_pubmd.models import Content
7
-
8
-
9
- class CmsClientError(Exception): ...
10
-
11
-
12
- class MimirCmsClient:
13
- base_url: str
14
-
15
- def __init__(self, base_url: str) -> None:
16
- self.base_url = base_url
17
-
18
- def _convert_preview_url(self, url_from_response: str) -> str:
19
- """Convert the preview URL to a full URL if it's relative."""
20
- url = urlparse(url_from_response)
21
- if url.scheme and url.netloc:
22
- return url.geturl()
23
- else:
24
- return urlparse(self.base_url)._replace(path=url.path).geturl()
25
-
26
- def send(self, token: str, content: Content) -> CmsResponse:
27
- """Sends a request to the Enonic CMS, assumed to have the mimir application installed (currently this only works with the feature branch https://github.com/statisticsnorway/mimir/pull/3192)."""
28
- try:
29
- response = requests.post(
30
- f"{self.base_url}/_/service/mimir/postMarkdown",
31
- headers={
32
- "Authorization": f"Bearer {token}",
33
- "Content-Type": "application/json",
34
- },
35
- json=content.to_json(),
36
- )
37
- if response.status_code != 200:
38
- raise CmsClientError(
39
- f"Request to CMS failed with status code {response.status_code}."
40
- )
41
- body = response.json()
42
- return CmsResponse(
43
- id=body["_id"],
44
- preview_url=self._convert_preview_url(body["previewPath"]),
45
- )
46
- except Exception as e:
47
- raise CmsClientError("Request to CMS failed.") from e
@@ -1,72 +0,0 @@
1
- import json
2
- from dataclasses import dataclass
3
- from pathlib import Path
4
-
5
- import nbformat
6
-
7
- from ssb_pubmd.models import Content
8
-
9
- ID_KEY = "_id"
10
-
11
-
12
- class LocalStorageError(Exception): ...
13
-
14
-
15
- @dataclass
16
- class LocalStorageAdapter:
17
- metadata_file_path: Path
18
-
19
- def get_file_id(self, file_path: Path) -> str:
20
- """
21
- Returns the content id of a given file path.
22
- If no id is registered for the given file path,
23
- it returns an empty string.
24
- """
25
- with open(self.metadata_file_path) as metadata_file:
26
- metadata = json.load(metadata_file)
27
- return str(metadata.get(str(file_path.absolute()), {}).get(ID_KEY, ""))
28
-
29
- def set_file_id(self, file_path: Path, content_id: str) -> None:
30
- """Stores a given file's content id in the metadata file"""
31
- with open(self.metadata_file_path) as metadata_file:
32
- metadata = json.load(metadata_file)
33
- metadata[str(file_path.absolute())] = {ID_KEY: content_id}
34
- with open(self.metadata_file_path, "w") as metadata_file:
35
- json.dump(metadata, metadata_file)
36
-
37
- def get_content(self, file_path: Path) -> Content:
38
- """
39
- Returns the markdown content of a given file.
40
- If the file is neither a .md or a .ipynb file,
41
- this function will throw a `LocalStorageError`
42
- """
43
- return Content(
44
- content_id=self.get_file_id(file_path),
45
- file_path=file_path,
46
- markdown=self._get_content(file_path),
47
- )
48
-
49
- def _get_content_from_notebook_file(self, file_path: Path) -> str:
50
- """Extracts all markdown cells from the notebook and returns it as a string."""
51
- notebook = nbformat.read(file_path, as_version=nbformat.NO_CONVERT) # type: ignore
52
- markdown_cells = []
53
- for cell in notebook.cells:
54
- if cell.cell_type == "markdown":
55
- markdown_cells.append(cell.source)
56
- sep = "\n\n"
57
- return sep.join(markdown_cells)
58
-
59
- def _get_content_from_markdown_file(self, file_path: Path) -> str:
60
- """Returns the content of a markdown file as a string."""
61
- with open(file_path) as file:
62
- return file.read()
63
-
64
- def _get_content(self, file_path: Path) -> str:
65
- file_type = file_path.suffix
66
- match file_type:
67
- case ".md":
68
- return self._get_content_from_markdown_file(file_path)
69
- case ".ipynb":
70
- return self._get_content_from_notebook_file(file_path)
71
- case _:
72
- raise LocalStorageError(f"Unsupported file type: {file_type}")
@@ -1,66 +0,0 @@
1
- """This module handles HTTP requests and responses to and from the CMS."""
2
-
3
- import json
4
- from dataclasses import dataclass
5
- from datetime import datetime
6
-
7
- import jwt
8
- from google.cloud import secretmanager
9
-
10
-
11
- @dataclass
12
- class Secret:
13
- private_key: str
14
- kid: str
15
- principal_key: str
16
-
17
-
18
- class SecretManagerError(Exception): ...
19
-
20
-
21
- class GoogleSecretManagerClient:
22
- TYPE = "JWT"
23
- ALGORITHM = "RS256"
24
- _gc_secret_resource_name: str
25
-
26
- def __init__(self, gc_secret_resource_name: str) -> None:
27
- self._gc_secret_resource_name = gc_secret_resource_name
28
-
29
- def _get_secret(self) -> Secret:
30
- """Fetches the private key and related data from Google Cloud Secret Manager."""
31
- client = secretmanager.SecretManagerServiceClient()
32
- response = client.access_secret_version(name=self._gc_secret_resource_name)
33
- raw_data = response.payload.data.decode("UTF-8")
34
- data = json.loads(raw_data)
35
- try:
36
- return Secret(
37
- private_key=data["privateKey"],
38
- kid=data["kid"],
39
- principal_key=data["principalKey"],
40
- )
41
- except KeyError as e:
42
- raise SecretManagerError(
43
- "The secret must be a JSON object with keys 'privateKey', 'kid' and 'principalKey'."
44
- ) from e
45
-
46
- def generate_token(self) -> str:
47
- secret = self._get_secret()
48
-
49
- header = {
50
- "kid": secret.kid,
51
- "typ": self.TYPE,
52
- "alg": self.ALGORITHM,
53
- }
54
-
55
- iat = int(datetime.now().timestamp())
56
- exp = iat + 30
57
- payload = {
58
- "sub": secret.principal_key,
59
- "iat": iat,
60
- "exp": exp,
61
- }
62
-
63
- token = jwt.encode(
64
- payload, secret.private_key, algorithm=self.ALGORITHM, headers=header
65
- )
66
- return token
@@ -1,30 +0,0 @@
1
- from dataclasses import dataclass
2
- from pathlib import Path
3
-
4
- from ssb_pubmd.config import Config
5
- from ssb_pubmd.ports import CmsClient
6
- from ssb_pubmd.ports import ContentFileHandler
7
- from ssb_pubmd.ports import SecretManagerClient
8
-
9
-
10
- @dataclass
11
- class EnonicCmsManager:
12
- """A CMS Mananager tailored to the Enonic CMS."""
13
-
14
- config: Config
15
- cms_client: CmsClient
16
- secret_manager_client: SecretManagerClient
17
- content_file_handler: ContentFileHandler
18
-
19
- def sync(self, content_file_path: str) -> str:
20
- """Requests that Enonic stores/updates the given contant file and gives back a rendered preview.
21
-
22
- The details of the communication are handled by the CmsClient implementation, which in turn depends on the services that are exposed by the Enonic XP application. The only thing this class cares is that it receives a CmsResponse object, which contains an id and preview url of the content.
23
- """
24
- content = self.content_file_handler.get_content(Path(content_file_path))
25
- response = self.cms_client.send(
26
- token=self.secret_manager_client.generate_token(),
27
- content=content,
28
- )
29
- self.content_file_handler.set_file_id(content.file_path, response.id)
30
- return response.preview_url
ssb_pubmd/models.py DELETED
@@ -1,28 +0,0 @@
1
- from dataclasses import dataclass
2
- from pathlib import Path
3
-
4
-
5
- @dataclass
6
- class CmsResponse:
7
- id: str
8
- preview_url: str
9
-
10
-
11
- @dataclass
12
- class Content:
13
- content_id: str
14
- file_path: Path
15
- markdown: str
16
-
17
- @property
18
- def display_name(self) -> str:
19
- """Generate a display name for the content."""
20
- return self.file_path.stem.replace("_", " ").title()
21
-
22
- def to_json(self) -> dict[str, str]:
23
- """Returns a json representation of the content."""
24
- return {
25
- "_id": self.content_id,
26
- "displayName": self.display_name,
27
- "markdown": self.markdown,
28
- }