ssb-pubmd 0.0.19__py3-none-any.whl → 0.1.1__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.
- ssb_pubmd/__init__.py +3 -5
- ssb_pubmd/__main__.py +7 -157
- ssb_pubmd/adapters/content_parser.py +185 -0
- ssb_pubmd/adapters/document_processor.py +149 -0
- ssb_pubmd/adapters/publish_client.py +124 -0
- ssb_pubmd/adapters/storage.py +42 -0
- ssb_pubmd/cli.py +78 -0
- ssb_pubmd/config.py +23 -0
- ssb_pubmd/domain/document_publisher.py +46 -0
- ssb_pubmd/notebook_client.py +130 -0
- {ssb_pubmd-0.0.19.dist-info → ssb_pubmd-0.1.1.dist-info}/METADATA +19 -22
- ssb_pubmd-0.1.1.dist-info/RECORD +16 -0
- {ssb_pubmd-0.0.19.dist-info → ssb_pubmd-0.1.1.dist-info}/WHEEL +1 -1
- ssb_pubmd-0.1.1.dist-info/entry_points.txt +3 -0
- ssb_pubmd/browser_request_handler.py +0 -85
- ssb_pubmd/constants.py +0 -22
- ssb_pubmd/jwt_request_handler.py +0 -99
- ssb_pubmd/markdown_syncer.py +0 -183
- ssb_pubmd/request_handler.py +0 -56
- ssb_pubmd-0.0.19.dist-info/RECORD +0 -13
- ssb_pubmd-0.0.19.dist-info/entry_points.txt +0 -3
- {ssb_pubmd-0.0.19.dist-info → ssb_pubmd-0.1.1.dist-info}/LICENSE +0 -0
ssb_pubmd/markdown_syncer.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import nbformat
|
|
5
|
-
from nbformat import NotebookNode
|
|
6
|
-
|
|
7
|
-
from ssb_pubmd.constants import METADATA_FILE
|
|
8
|
-
from ssb_pubmd.constants import ContentType
|
|
9
|
-
from ssb_pubmd.request_handler import RequestHandler
|
|
10
|
-
from ssb_pubmd.request_handler import Response
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class MarkdownSyncer:
|
|
14
|
-
"""This class syncs a content file to a CMS (Content Management System).
|
|
15
|
-
|
|
16
|
-
The CMS must have an endpoint that satisfies the following constraints:
|
|
17
|
-
|
|
18
|
-
- It must accept a post request with fields *_id*, *displayName* and *markdown*.
|
|
19
|
-
- The response body must have a key *_id* whose value should be
|
|
20
|
-
a unique string identifier of the content.
|
|
21
|
-
|
|
22
|
-
Creating and updating content is handled in the following way:
|
|
23
|
-
|
|
24
|
-
- On the first request, an empty string is sent as *_id*.
|
|
25
|
-
- If the request succeeds, the value of *_id* (in the response) is stored in a JSON file
|
|
26
|
-
(created in the same directory as the markdown/notebook file).
|
|
27
|
-
- On subsequent requests, the stored value is sent as *_id*.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
ID_KEY = "_id"
|
|
31
|
-
|
|
32
|
-
def __init__(
|
|
33
|
-
self,
|
|
34
|
-
post_url: str,
|
|
35
|
-
request_handler: RequestHandler,
|
|
36
|
-
metadata_file: Path = METADATA_FILE,
|
|
37
|
-
) -> None:
|
|
38
|
-
"""Creates a markdown syncer instance that connects to the CMS through the post url."""
|
|
39
|
-
self._post_url: str = post_url
|
|
40
|
-
self._request_handler: RequestHandler = request_handler
|
|
41
|
-
self._content_file_path: Path = Path()
|
|
42
|
-
self._content_file_type: ContentType = ContentType.MARKDOWN
|
|
43
|
-
self._metadata_file_path: Path = metadata_file
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def content_file_path(self) -> Path:
|
|
47
|
-
"""Returns the path of the content file."""
|
|
48
|
-
return self._content_file_path
|
|
49
|
-
|
|
50
|
-
@content_file_path.setter
|
|
51
|
-
def content_file_path(self, path: Path) -> None:
|
|
52
|
-
"""Sets the path of the content file."""
|
|
53
|
-
if not path.is_file():
|
|
54
|
-
raise FileNotFoundError(f"The file {path} does not exist.")
|
|
55
|
-
|
|
56
|
-
ext = path.suffix.lower()
|
|
57
|
-
for t in ContentType:
|
|
58
|
-
if ext == t.value:
|
|
59
|
-
self._content_file_type = t
|
|
60
|
-
break
|
|
61
|
-
else:
|
|
62
|
-
allowed_extensions = [t.value for t in ContentType]
|
|
63
|
-
sep = ", "
|
|
64
|
-
raise ValueError(
|
|
65
|
-
f"The file {path} has extension {ext}, but should be one of: {sep.join(allowed_extensions)}."
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
self._content_file_path = path
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def basename(self) -> str:
|
|
72
|
-
"""The name of the content file without extension."""
|
|
73
|
-
return self._content_file_path.stem
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def display_name(self) -> str:
|
|
77
|
-
"""Generate a display name for the content."""
|
|
78
|
-
return self.basename.replace("_", " ").title()
|
|
79
|
-
|
|
80
|
-
@property
|
|
81
|
-
def metadata_file_path(self) -> Path:
|
|
82
|
-
"""The path of the metadata file."""
|
|
83
|
-
return self._metadata_file_path
|
|
84
|
-
|
|
85
|
-
@property
|
|
86
|
-
def metadata_key(self) -> str:
|
|
87
|
-
"""The key that the content metadata will be stored under in the metadata file."""
|
|
88
|
-
return str(self._content_file_path.absolute())
|
|
89
|
-
|
|
90
|
-
def _save_content_id(self, content_id: str) -> None:
|
|
91
|
-
"""Saves the content id to the metadata file."""
|
|
92
|
-
with open(self._metadata_file_path) as f:
|
|
93
|
-
try:
|
|
94
|
-
data = json.load(f)
|
|
95
|
-
except json.JSONDecodeError:
|
|
96
|
-
data = {}
|
|
97
|
-
|
|
98
|
-
data[self.metadata_key] = {
|
|
99
|
-
self.ID_KEY: content_id,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
with open(self._metadata_file_path, "w") as f:
|
|
103
|
-
json.dump(data, f, indent=4)
|
|
104
|
-
|
|
105
|
-
def _get_content_id(self) -> str:
|
|
106
|
-
"""Fetches the content id from the metadata file if it exists, otherwise an empty string."""
|
|
107
|
-
with open(self._metadata_file_path) as f:
|
|
108
|
-
try:
|
|
109
|
-
data = json.load(f)
|
|
110
|
-
except json.JSONDecodeError:
|
|
111
|
-
data = {}
|
|
112
|
-
|
|
113
|
-
metadata: dict[str, str] = data.get(self.metadata_key, {})
|
|
114
|
-
|
|
115
|
-
content_id = metadata.get(self.ID_KEY, "")
|
|
116
|
-
|
|
117
|
-
return content_id
|
|
118
|
-
|
|
119
|
-
def _read_notebook(self) -> NotebookNode:
|
|
120
|
-
"""Reads the notebook file and returns its content."""
|
|
121
|
-
return nbformat.read(self._content_file_path, as_version=nbformat.NO_CONVERT) # type: ignore
|
|
122
|
-
|
|
123
|
-
def _get_content_from_notebook_file(self) -> str:
|
|
124
|
-
"""Extracts all markdown cells from the notebook and returns it as a merged string."""
|
|
125
|
-
notebook = self._read_notebook()
|
|
126
|
-
|
|
127
|
-
markdown_cells = []
|
|
128
|
-
for cell in notebook.cells:
|
|
129
|
-
if cell.cell_type == "markdown":
|
|
130
|
-
markdown_cells.append(cell.source)
|
|
131
|
-
|
|
132
|
-
markdown_content = "\n\n".join(markdown_cells)
|
|
133
|
-
|
|
134
|
-
return markdown_content
|
|
135
|
-
|
|
136
|
-
def _get_content_from_markdown_file(self) -> str:
|
|
137
|
-
"""Returns the content of a markdown file."""
|
|
138
|
-
with open(self._content_file_path) as file:
|
|
139
|
-
markdown_content = file.read()
|
|
140
|
-
return markdown_content
|
|
141
|
-
|
|
142
|
-
def _get_content(self) -> str:
|
|
143
|
-
content = ""
|
|
144
|
-
match self._content_file_type:
|
|
145
|
-
case ContentType.MARKDOWN:
|
|
146
|
-
content = self._get_content_from_markdown_file()
|
|
147
|
-
case ContentType.NOTEBOOK:
|
|
148
|
-
content = self._get_content_from_notebook_file()
|
|
149
|
-
return content
|
|
150
|
-
|
|
151
|
-
def _request_data(self) -> dict[str, str]:
|
|
152
|
-
"""Prepares the request data to be sent to the CMS endpoint."""
|
|
153
|
-
return {
|
|
154
|
-
"_id": self._get_content_id(),
|
|
155
|
-
"displayName": self.display_name,
|
|
156
|
-
"markdown": self._get_content(),
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
def sync_content(self) -> Response:
|
|
160
|
-
"""Sends the request to the CMS endpoint and returns the content id from the response."""
|
|
161
|
-
response = self._request_handler.send_request(
|
|
162
|
-
url=self._post_url, data=self._request_data()
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
if response.status_code != 200:
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f"Request to the CMS failed with status code {response.status_code}."
|
|
168
|
-
)
|
|
169
|
-
if response.body is None:
|
|
170
|
-
raise ValueError("Response body from CMS could not be parsed.")
|
|
171
|
-
if self.ID_KEY not in response.body:
|
|
172
|
-
raise ValueError(
|
|
173
|
-
f"Response from the CMS does not contain the expected key '{self.ID_KEY}'."
|
|
174
|
-
)
|
|
175
|
-
result = response.body[self.ID_KEY]
|
|
176
|
-
if not isinstance(result, str):
|
|
177
|
-
raise ValueError(
|
|
178
|
-
f"Response from the CMS does not contain a valid content id: {result}"
|
|
179
|
-
)
|
|
180
|
-
content_id: str = result
|
|
181
|
-
self._save_content_id(content_id)
|
|
182
|
-
|
|
183
|
-
return response
|
ssb_pubmd/request_handler.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Any
|
|
3
|
-
from typing import Protocol
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass
|
|
9
|
-
class Response:
|
|
10
|
-
"""The response object used in the package."""
|
|
11
|
-
|
|
12
|
-
status_code: int
|
|
13
|
-
body: dict[str, Any]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class RequestHandler(Protocol):
|
|
17
|
-
"""Interface for handling how a request are sent.
|
|
18
|
-
|
|
19
|
-
Implementing classes may handle authentication, sessions, etc.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def send_request(
|
|
23
|
-
self,
|
|
24
|
-
url: str,
|
|
25
|
-
headers: dict[str, str] | None = None,
|
|
26
|
-
data: dict[str, str] | None = None,
|
|
27
|
-
) -> Response:
|
|
28
|
-
"""Sends the request to the specified url, optionally with headers and data, and returns the response."""
|
|
29
|
-
...
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class BasicRequestHandler:
|
|
33
|
-
"""Basic, unauthenticated request handler."""
|
|
34
|
-
|
|
35
|
-
def send_request(
|
|
36
|
-
self,
|
|
37
|
-
url: str,
|
|
38
|
-
headers: dict[str, str] | None = None,
|
|
39
|
-
data: dict[str, str] | None = None,
|
|
40
|
-
) -> Response:
|
|
41
|
-
"""Sends the request to the specified url without any headers."""
|
|
42
|
-
response = requests.post(
|
|
43
|
-
url,
|
|
44
|
-
data=data,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
body = response.json()
|
|
49
|
-
body = dict(body)
|
|
50
|
-
except Exception:
|
|
51
|
-
body = {}
|
|
52
|
-
|
|
53
|
-
return Response(
|
|
54
|
-
status_code=response.status_code,
|
|
55
|
-
body=body,
|
|
56
|
-
)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
ssb_pubmd/__init__.py,sha256=hWhlVS_FDH9yq7fBdXwggg5opjZwf7gIKY6ZyJ81Y4w,176
|
|
2
|
-
ssb_pubmd/__main__.py,sha256=iIjz73oLP323XyF9cB4LAvO8C47oFhIGDztuzJTOjYo,5089
|
|
3
|
-
ssb_pubmd/browser_request_handler.py,sha256=rfTzsXpKNikH7HchzGkoV09O5bsxmpCB-JGzJ-_lyBc,2902
|
|
4
|
-
ssb_pubmd/constants.py,sha256=0Fh9dOX0wN9spON96Zk8UIhh2yCmTgJVZd2W2uYdXqk,565
|
|
5
|
-
ssb_pubmd/jwt_request_handler.py,sha256=Q3Da2R3tJwh1w40bESm90-JZwXfeYvu6R3K6cQ4-ySM,2844
|
|
6
|
-
ssb_pubmd/markdown_syncer.py,sha256=2MCUlVofXDfUpNCoxz4byfVwc9n_eNABcaOfgxKvMy4,6535
|
|
7
|
-
ssb_pubmd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
ssb_pubmd/request_handler.py,sha256=yjQaIXi3sRoyt9I4MOjfOXvco_VHPWB6OAMRapCH1GY,1318
|
|
9
|
-
ssb_pubmd-0.0.19.dist-info/LICENSE,sha256=tF5bnYv09fgH5ph9t1EpH1MGrVOGTQeswL4dzVeZ_ak,1073
|
|
10
|
-
ssb_pubmd-0.0.19.dist-info/METADATA,sha256=m4JJpfLDtetdNIFxJz_BhII5ZK23FsgiVKTLBsa5Id8,4331
|
|
11
|
-
ssb_pubmd-0.0.19.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
12
|
-
ssb_pubmd-0.0.19.dist-info/entry_points.txt,sha256=1_NfsiOfqTg948JWXYPwi4QtDk90KHkNn1CQtye8rJ0,48
|
|
13
|
-
ssb_pubmd-0.0.19.dist-info/RECORD,,
|
|
File without changes
|