ssb-pubmd 0.0.16__tar.gz → 0.0.17__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.
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/PKG-INFO +3 -1
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/pyproject.toml +3 -1
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/__main__.py +38 -3
- ssb_pubmd-0.0.17/src/ssb_pubmd/jwt_request_handler.py +99 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/LICENSE +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/README.md +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/__init__.py +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/browser_request_handler.py +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/constants.py +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/markdown_syncer.py +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/py.typed +0 -0
- {ssb_pubmd-0.0.16 → ssb_pubmd-0.0.17}/src/ssb_pubmd/request_handler.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ssb-pubmd
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.17
|
|
4
4
|
Summary: SSB Pubmd
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Olav Landsverk
|
|
@@ -14,9 +14,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Dist: click (>=8.0.1)
|
|
17
|
+
Requires-Dist: google-cloud-secret-manager (>=2.24.0,<3.0.0)
|
|
17
18
|
Requires-Dist: nbformat (>=5.10.4,<6.0.0)
|
|
18
19
|
Requires-Dist: platformdirs (>=4.3.8,<5.0.0)
|
|
19
20
|
Requires-Dist: playwright (>=1.51.0,<2.0.0)
|
|
21
|
+
Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
|
|
20
22
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
21
23
|
Requires-Dist: types-requests (>=2.32.0.20250306,<3.0.0.0)
|
|
22
24
|
Project-URL: Changelog, https://github.com/statisticsnorway/ssb-pubmd/releases
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ssb-pubmd"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.17"
|
|
4
4
|
description = "SSB Pubmd"
|
|
5
5
|
authors = ["Olav Landsverk <stud-oll@ssb.no>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -21,6 +21,8 @@ requests = "^2.32.3"
|
|
|
21
21
|
types-requests = "^2.32.0.20250306"
|
|
22
22
|
playwright = "^1.51.0"
|
|
23
23
|
platformdirs = "^4.3.8"
|
|
24
|
+
pyjwt = "^2.10.1"
|
|
25
|
+
google-cloud-secret-manager = "^2.24.0"
|
|
24
26
|
|
|
25
27
|
[tool.poetry.group.dev.dependencies]
|
|
26
28
|
pygments = ">=2.10.0"
|
|
@@ -14,14 +14,18 @@ from ssb_pubmd.browser_request_handler import CreateContextMethod
|
|
|
14
14
|
from ssb_pubmd.constants import APP_NAME
|
|
15
15
|
from ssb_pubmd.constants import CACHE_FILE
|
|
16
16
|
from ssb_pubmd.constants import CONFIG_FILE
|
|
17
|
+
from ssb_pubmd.jwt_request_handler import JWTRequestHandler
|
|
17
18
|
from ssb_pubmd.markdown_syncer import MarkdownSyncer
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ConfigKey(Enum):
|
|
21
22
|
"""Configuration keys for the application."""
|
|
22
23
|
|
|
24
|
+
BASE_URL = "base_url"
|
|
23
25
|
LOGIN_URL = "login_url"
|
|
24
26
|
POST_URL = "post_url"
|
|
27
|
+
AUTH_METHOD = "auth_method"
|
|
28
|
+
GC_SECRET_RESOURCE_NAME = "gc_secret_resource_name"
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def get_config_value(config_key: ConfigKey) -> str:
|
|
@@ -94,9 +98,7 @@ def login() -> None:
|
|
|
94
98
|
click.echo(f"\nBrowser context stored in:\n{CACHE_FILE}")
|
|
95
99
|
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
@click.argument("content_file_path", type=click.Path())
|
|
99
|
-
def sync(content_file_path: str) -> None:
|
|
101
|
+
def sync_with_browser(content_file_path: str) -> None:
|
|
100
102
|
"""Sync a markdown or notebook file to the CMS."""
|
|
101
103
|
login_url = get_config_value(ConfigKey.LOGIN_URL)
|
|
102
104
|
request_handler = BrowserRequestHandler(CACHE_FILE, login_url)
|
|
@@ -122,5 +124,38 @@ def sync(content_file_path: str) -> None:
|
|
|
122
124
|
click.echo("No preview url found in the response.")
|
|
123
125
|
|
|
124
126
|
|
|
127
|
+
def sync_with_jwt(content_file_path: str) -> None:
|
|
128
|
+
"""Sync a markdown or notebook file to the CMS."""
|
|
129
|
+
gc_secret_resource_name = get_config_value(ConfigKey.GC_SECRET_RESOURCE_NAME)
|
|
130
|
+
request_handler = JWTRequestHandler(gc_secret_resource_name)
|
|
131
|
+
|
|
132
|
+
post_url = get_config_value(ConfigKey.POST_URL)
|
|
133
|
+
syncer = MarkdownSyncer(post_url, request_handler)
|
|
134
|
+
|
|
135
|
+
syncer.content_file_path = Path(content_file_path)
|
|
136
|
+
response = syncer.sync_content()
|
|
137
|
+
|
|
138
|
+
click.echo("Content synced successfully.")
|
|
139
|
+
|
|
140
|
+
preview_path = response.body.get("previewPath", "")
|
|
141
|
+
if preview_path:
|
|
142
|
+
base_url = get_config_value(ConfigKey.BASE_URL)
|
|
143
|
+
preview = urlparse(base_url)._replace(path=preview_path).geturl()
|
|
144
|
+
click.echo(f"Preview url found in the response: {preview}")
|
|
145
|
+
else:
|
|
146
|
+
click.echo("No preview url found in the response.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@cli.command()
|
|
150
|
+
@click.argument("content_file_path", type=click.Path())
|
|
151
|
+
def sync(content_file_path: str) -> None:
|
|
152
|
+
"""Sync a markdown or notebook file to the CMS."""
|
|
153
|
+
auth_method = get_config_value(ConfigKey.AUTH_METHOD)
|
|
154
|
+
if auth_method == "browser":
|
|
155
|
+
sync_with_browser(content_file_path)
|
|
156
|
+
else:
|
|
157
|
+
sync_with_jwt(content_file_path)
|
|
158
|
+
|
|
159
|
+
|
|
125
160
|
if __name__ == "__main__":
|
|
126
161
|
cli()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import jwt
|
|
6
|
+
import requests
|
|
7
|
+
from google.cloud import secretmanager
|
|
8
|
+
|
|
9
|
+
from ssb_pubmd.request_handler import Response
|
|
10
|
+
|
|
11
|
+
TYPE = "JWT"
|
|
12
|
+
ALGORITHM = "RS256"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SecretData:
|
|
17
|
+
"""Data class to hold private key and connected data."""
|
|
18
|
+
|
|
19
|
+
private_key: str
|
|
20
|
+
kid: str
|
|
21
|
+
principal_key: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JWTRequestHandler:
|
|
25
|
+
"""This class is used to send requests with a JSON Web Token (JWT) in the header."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, gc_secret_resource_name: str) -> None:
|
|
28
|
+
"""Initializes a JWT request handler object."""
|
|
29
|
+
self._gc_secret_resource_name: str = gc_secret_resource_name
|
|
30
|
+
|
|
31
|
+
def _private_key_from_secret_manager(self) -> SecretData:
|
|
32
|
+
"""Fetches the private key from Google Cloud Secret Manager."""
|
|
33
|
+
client = secretmanager.SecretManagerServiceClient()
|
|
34
|
+
print(f"Fetching secret from {self._gc_secret_resource_name}")
|
|
35
|
+
response = client.access_secret_version(name=self._gc_secret_resource_name)
|
|
36
|
+
raw_data = response.payload.data.decode("UTF-8")
|
|
37
|
+
data = json.loads(raw_data)
|
|
38
|
+
try:
|
|
39
|
+
secret_data = SecretData(
|
|
40
|
+
private_key=data["privateKey"],
|
|
41
|
+
kid=data["kid"],
|
|
42
|
+
principal_key=data["principalKey"],
|
|
43
|
+
)
|
|
44
|
+
except KeyError as e:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"The secret must be a JSON object with keys 'privateKey', 'kid' and 'principalKey'."
|
|
47
|
+
) from e
|
|
48
|
+
return secret_data
|
|
49
|
+
|
|
50
|
+
def _generate_token(self) -> str:
|
|
51
|
+
secret_data = self._private_key_from_secret_manager()
|
|
52
|
+
|
|
53
|
+
header = {
|
|
54
|
+
"kid": secret_data.kid,
|
|
55
|
+
"typ": TYPE,
|
|
56
|
+
"alg": ALGORITHM,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
iat = int(datetime.now().timestamp())
|
|
60
|
+
exp = iat + 30
|
|
61
|
+
payload = {
|
|
62
|
+
"sub": secret_data.principal_key,
|
|
63
|
+
"iat": iat,
|
|
64
|
+
"exp": exp,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
token = jwt.encode(
|
|
68
|
+
payload, secret_data.private_key, algorithm=ALGORITHM, headers=header
|
|
69
|
+
)
|
|
70
|
+
return token
|
|
71
|
+
|
|
72
|
+
def send_request(
|
|
73
|
+
self,
|
|
74
|
+
url: str,
|
|
75
|
+
headers: dict[str, str] | None = None,
|
|
76
|
+
data: dict[str, str] | None = None,
|
|
77
|
+
) -> Response:
|
|
78
|
+
"""Sends the request to the specified url with bearer token in header."""
|
|
79
|
+
token = self._generate_token()
|
|
80
|
+
headers = {
|
|
81
|
+
"Authorization": f"Bearer {token}",
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
}
|
|
84
|
+
response = requests.post(
|
|
85
|
+
url,
|
|
86
|
+
headers=headers,
|
|
87
|
+
json=data,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
body = response.json()
|
|
92
|
+
body = dict(body)
|
|
93
|
+
except Exception:
|
|
94
|
+
body = {}
|
|
95
|
+
|
|
96
|
+
return Response(
|
|
97
|
+
status_code=response.status_code,
|
|
98
|
+
body=body,
|
|
99
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|