ssb-pubmd 0.0.15__py3-none-any.whl → 0.0.17__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/__main__.py CHANGED
@@ -2,106 +2,160 @@
2
2
 
3
3
  import json
4
4
  import os
5
- from dataclasses import asdict
6
- from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import cast
8
+ from urllib.parse import urlparse
7
9
 
8
10
  import click
9
11
 
10
12
  from ssb_pubmd.browser_request_handler import BrowserRequestHandler
11
13
  from ssb_pubmd.browser_request_handler import CreateContextMethod
14
+ from ssb_pubmd.constants import APP_NAME
15
+ from ssb_pubmd.constants import CACHE_FILE
16
+ from ssb_pubmd.constants import CONFIG_FILE
17
+ from ssb_pubmd.jwt_request_handler import JWTRequestHandler
12
18
  from ssb_pubmd.markdown_syncer import MarkdownSyncer
13
19
 
14
- BASE_DIR = os.path.join(os.path.expanduser("~"), ".pubmd")
15
- os.makedirs(BASE_DIR, exist_ok=True)
16
20
 
17
- CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
18
- BROWSER_CONTEXT_PATH = os.path.join(BASE_DIR, "browser_context.json")
21
+ class ConfigKey(Enum):
22
+ """Configuration keys for the application."""
19
23
 
24
+ BASE_URL = "base_url"
25
+ LOGIN_URL = "login_url"
26
+ POST_URL = "post_url"
27
+ AUTH_METHOD = "auth_method"
28
+ GC_SECRET_RESOURCE_NAME = "gc_secret_resource_name"
20
29
 
21
- @dataclass
22
- class Config:
23
- """Handles the user configuration."""
24
30
 
25
- login_url: str = ""
26
- post_url: str = ""
31
+ def get_config_value(config_key: ConfigKey) -> str:
32
+ """Load a configuration value, with precedence environment variable > config file."""
33
+ key = config_key.value
27
34
 
28
- @classmethod
29
- def load(cls, path: str) -> "Config":
30
- """Loads the configuration from a file."""
31
- with open(path) as f:
35
+ def get_env_value() -> str:
36
+ """Get value from environment variable, by uppercasing the key and adding prefix."""
37
+ prefix = f"{APP_NAME.upper()}_"
38
+ value = os.getenv(f"{prefix}{key.upper()}")
39
+
40
+ return cast(str, value)
41
+
42
+ def get_config_file_value() -> str:
43
+ """Get value from the config file."""
44
+ with open(CONFIG_FILE) as f:
32
45
  data = json.load(f)
33
- return cls(**data)
34
46
 
35
- def save(self, path: str) -> None:
36
- """Saves the configuration to a file."""
37
- with open(path, "w") as f:
38
- json.dump(asdict(self), f, indent=4)
47
+ value = data.get(key)
39
48
 
49
+ return cast(str, value)
40
50
 
41
- @click.command()
42
- def config() -> None:
43
- """Set the login and post URL for the CMS."""
44
- config_file = CONFIG_PATH
51
+ return get_env_value() or get_config_file_value()
45
52
 
46
- login_url = click.prompt("Enter the login URL", type=str)
47
- post_url = click.prompt("Enter the post URL", type=str)
48
53
 
49
- config = Config(login_url=login_url, post_url=post_url)
54
+ def set_config_value(config_key: ConfigKey, value: str) -> None:
55
+ """Set a configuration value in the config file."""
56
+ key = config_key.value
57
+
58
+ with open(CONFIG_FILE) as f:
59
+ try:
60
+ data = json.load(f)
61
+ except json.JSONDecodeError:
62
+ data = {}
63
+
64
+ data[key] = value
65
+
66
+ with open(CONFIG_FILE, "w") as f:
67
+ json.dump(data, f, indent=4)
68
+
69
+
70
+ @click.group()
71
+ def cli() -> None:
72
+ """Pubmd - a tool to sync markdown and notebook files to a CMS."""
73
+ pass
74
+
75
+
76
+ @cli.command()
77
+ def settings() -> None:
78
+ """Set the login and post URL for the CMS."""
79
+ login_url = click.prompt("Enter the login URL", type=str)
80
+ set_config_value(ConfigKey.LOGIN_URL, login_url)
50
81
 
51
- config.save(config_file)
82
+ post_url = click.prompt("Enter the post URL", type=str)
83
+ set_config_value(ConfigKey.POST_URL, post_url)
52
84
 
53
- click.echo(f"\nConfiguration stored in:\n{config_file}")
85
+ click.echo(f"\nSettings stored in:\n{click.format_filename(CONFIG_FILE)}")
54
86
 
55
87
 
56
- @click.command()
88
+ @cli.command()
57
89
  def login() -> None:
58
90
  """Log in to the CMS application."""
59
- config_file = CONFIG_PATH
60
- browser_context_file = BROWSER_CONTEXT_PATH
61
-
62
- config = Config.load(config_file)
63
- request_handler = BrowserRequestHandler(browser_context_file, config.login_url)
91
+ login_url = get_config_value(ConfigKey.LOGIN_URL)
92
+ request_handler = BrowserRequestHandler(CACHE_FILE, login_url)
64
93
 
65
94
  method = CreateContextMethod.FROM_LOGIN
66
95
  with request_handler.new_context(method=method):
67
96
  click.echo("Logging in...")
68
97
 
69
- click.echo(f"\nBrowser context stored in:\n{browser_context_file}")
98
+ click.echo(f"\nBrowser context stored in:\n{CACHE_FILE}")
70
99
 
71
100
 
72
- @click.command()
73
- @click.argument("content_file_path", type=click.Path())
74
- def sync(content_file_path: str) -> None:
101
+ def sync_with_browser(content_file_path: str) -> None:
75
102
  """Sync a markdown or notebook file to the CMS."""
76
- config_file = CONFIG_PATH
77
- browser_context_file = BROWSER_CONTEXT_PATH
103
+ login_url = get_config_value(ConfigKey.LOGIN_URL)
104
+ request_handler = BrowserRequestHandler(CACHE_FILE, login_url)
78
105
 
79
- request_handler = BrowserRequestHandler(browser_context_file)
106
+ with request_handler.new_context() as context:
107
+ post_url = get_config_value(ConfigKey.POST_URL)
108
+ syncer = MarkdownSyncer(post_url, request_handler)
80
109
 
81
- with request_handler.new_context():
82
- config = Config.load(config_file)
83
- syncer = MarkdownSyncer(
84
- post_url=config.post_url, request_handler=request_handler
85
- )
110
+ syncer.content_file_path = Path(content_file_path)
111
+ response = syncer.sync_content()
86
112
 
87
- syncer.content_file_path = content_file_path
88
- content_id = syncer.sync_content()
113
+ click.echo("Content synced successfully.")
89
114
 
90
- click.echo(
91
- f"File '{click.format_filename(browser_context_file)}' synced to CMS with content ID: {content_id}."
92
- )
93
- click.echo(f"Response data saved to '{click.format_filename(syncer.data_path)}'.")
115
+ path = response.body.get("previewPath", "")
116
+ preview = urlparse(login_url)._replace(path=path).geturl()
117
+ if preview:
118
+ page = context.new_page()
119
+ page.goto(preview)
120
+ click.echo(f"Preview opened in new browser: {preview}")
121
+ click.echo("Close the browser tab to finish.")
122
+ page.wait_for_event("close", timeout=0)
123
+ else:
124
+ click.echo("No preview url found in the response.")
94
125
 
95
126
 
96
- @click.group()
97
- def cli() -> None:
98
- """Pubmd - a tool to sync markdown and notebook files to a CMS."""
99
- pass
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.")
100
147
 
101
148
 
102
- cli.add_command(config)
103
- cli.add_command(login)
104
- cli.add_command(sync)
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
+
105
159
 
106
160
  if __name__ == "__main__":
107
161
  cli()
@@ -1,12 +1,12 @@
1
- import os
2
1
  from collections.abc import Iterator
3
2
  from contextlib import contextmanager
4
3
  from enum import Enum
4
+ from pathlib import Path
5
5
 
6
6
  from playwright.sync_api import BrowserContext
7
7
  from playwright.sync_api import sync_playwright
8
8
 
9
- from .markdown_syncer import Response
9
+ from ssb_pubmd.request_handler import Response
10
10
 
11
11
 
12
12
  class CreateContextMethod(Enum):
@@ -23,9 +23,9 @@ class CreateContextMethod(Enum):
23
23
  class BrowserRequestHandler:
24
24
  """This class is used to create a logged in browser context from which to send requests."""
25
25
 
26
- def __init__(self, context_file_path: str, login_url: str = "") -> None:
26
+ def __init__(self, context_file_path: Path, login_url: str) -> None:
27
27
  """Initializes an empty browser context object."""
28
- self._context_file_path: str = os.path.abspath(context_file_path)
28
+ self._context_file_path: Path = context_file_path
29
29
  self._login_url: str = login_url
30
30
  self._context: BrowserContext | None = None
31
31
 
@@ -75,7 +75,7 @@ class BrowserRequestHandler:
75
75
  body = api_response.json()
76
76
  body = dict(body)
77
77
  except Exception:
78
- body = None
78
+ body = {}
79
79
 
80
80
  response = Response(
81
81
  status_code=api_response.status,
ssb_pubmd/constants.py ADDED
@@ -0,0 +1,22 @@
1
+ from enum import Enum
2
+
3
+ from platformdirs import user_cache_path
4
+ from platformdirs import user_config_path
5
+ from platformdirs import user_data_path
6
+
7
+ APP_NAME = "pubmd"
8
+
9
+ METADATA_FILE = user_data_path(APP_NAME, ensure_exists=True) / "metadata.json"
10
+ CACHE_FILE = user_cache_path(APP_NAME, ensure_exists=True) / "cache.json"
11
+ CONFIG_FILE = user_config_path(APP_NAME, ensure_exists=True) / "config.json"
12
+
13
+ CACHE_FILE.touch()
14
+ CONFIG_FILE.touch()
15
+ METADATA_FILE.touch()
16
+
17
+
18
+ class ContentType(Enum):
19
+ """Allowed content types."""
20
+
21
+ MARKDOWN = ".md"
22
+ NOTEBOOK = ".ipynb"
@@ -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
+ )
@@ -1,79 +1,17 @@
1
1
  import json
2
- import os
3
- from dataclasses import dataclass
4
- from enum import Enum
5
- from typing import Any
6
- from typing import Protocol
2
+ from pathlib import Path
7
3
 
8
4
  import nbformat
9
- import requests
10
5
  from nbformat import NotebookNode
11
6
 
12
-
13
- @dataclass
14
- class Response:
15
- """The expected response object used in this module."""
16
-
17
- status_code: int
18
- body: dict[str, Any] | None = None
19
-
20
-
21
- class RequestHandler(Protocol):
22
- """Interface for the handling how the request is sent.
23
-
24
- Implementing classes may handle authentication, sessions, etc.
25
- """
26
-
27
- def send_request(
28
- self,
29
- url: str,
30
- headers: dict[str, str] | None = None,
31
- data: dict[str, str] | None = None,
32
- ) -> Response:
33
- """Sends the request to the specified url, optionally with headers and data, and returns the response."""
34
- ...
35
-
36
-
37
- class BasicRequestHandler:
38
- """Basic, unauthenticated request handler."""
39
-
40
- def __init__(self) -> None:
41
- """Initializes the basic request handler."""
42
- pass
43
-
44
- def send_request(
45
- self,
46
- url: str,
47
- headers: dict[str, str] | None = None,
48
- data: dict[str, str] | None = None,
49
- ) -> Response:
50
- """Sends the request to the specified url without any headers."""
51
- response = requests.post(
52
- url,
53
- data=data,
54
- )
55
-
56
- try:
57
- body = response.json()
58
- body = dict(body)
59
- except Exception:
60
- body = None
61
-
62
- return Response(
63
- status_code=response.status_code,
64
- body=body,
65
- )
66
-
67
-
68
- class FileType(Enum):
69
- """File extensions for markdown and notebook files."""
70
-
71
- MARKDOWN = ".md"
72
- NOTEBOOK = ".ipynb"
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
73
11
 
74
12
 
75
13
  class MarkdownSyncer:
76
- """This class syncs a markdown/notebook file to a CMS (Content Management System).
14
+ """This class syncs a content file to a CMS (Content Management System).
77
15
 
78
16
  The CMS must have an endpoint that satisfies the following constraints:
79
17
 
@@ -91,68 +29,91 @@ class MarkdownSyncer:
91
29
 
92
30
  ID_KEY = "_id"
93
31
 
94
- def __init__(self, post_url: str, request_handler: RequestHandler) -> None:
32
+ def __init__(
33
+ self,
34
+ post_url: str,
35
+ request_handler: RequestHandler,
36
+ metadata_file: Path = METADATA_FILE,
37
+ ) -> None:
95
38
  """Creates a markdown syncer instance that connects to the CMS through the post url."""
96
39
  self._post_url: str = post_url
97
40
  self._request_handler: RequestHandler = request_handler
98
- self._content_file_path: str = ""
99
- self._content_file_type: FileType = FileType.MARKDOWN
41
+ self._content_file_path: Path = Path()
42
+ self._content_file_type: ContentType = ContentType.MARKDOWN
43
+ self._metadata_file_path: Path = metadata_file
100
44
 
101
45
  @property
102
- def content_file_path(self) -> str:
103
- """Returns the path of the markdown/notebook file."""
46
+ def content_file_path(self) -> Path:
47
+ """Returns the path of the content file."""
104
48
  return self._content_file_path
105
49
 
106
50
  @content_file_path.setter
107
- def content_file_path(self, content_file_path: str) -> None:
108
- """Sets the path of the markdown/notebook file."""
109
- content_file_path = os.path.abspath(content_file_path)
110
-
111
- if not os.path.exists(content_file_path):
112
- raise FileNotFoundError(f"The file '{content_file_path}' does not exist.")
113
-
114
- ext = os.path.splitext(content_file_path)[1]
115
- for e in FileType:
116
- if ext == e.value:
117
- self._content_file_type = e
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
118
60
  break
119
61
  else:
62
+ allowed_extensions = [t.value for t in ContentType]
63
+ sep = ", "
120
64
  raise ValueError(
121
- f"The file '{content_file_path}' is not a markdown or notebook file."
65
+ f"The file {path} has extension {ext}, but should be one of: {sep.join(allowed_extensions)}."
122
66
  )
123
67
 
124
- self._content_file_path = content_file_path
68
+ self._content_file_path = path
125
69
 
126
70
  @property
127
71
  def basename(self) -> str:
128
- """The name of the markdown/notebook file without extension."""
129
- basename = os.path.basename(self.content_file_path)
130
- return os.path.splitext(basename)[0]
131
-
132
- @property
133
- def data_path(self) -> str:
134
- """The absolute path of the file to store the data returned from the CMS."""
135
- return os.path.splitext(self.content_file_path)[0] + "-PUBMD.json"
72
+ """The name of the content file without extension."""
73
+ return self._content_file_path.stem
136
74
 
137
75
  @property
138
76
  def display_name(self) -> str:
139
77
  """Generate a display name for the content."""
140
78
  return self.basename.replace("_", " ").title()
141
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
+
142
90
  def _save_content_id(self, content_id: str) -> None:
143
- """Saves the content id to the data file."""
144
- filename = self.data_path
145
- with open(filename, "w") as file:
146
- json.dump({self.ID_KEY: content_id}, file)
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)
147
104
 
148
105
  def _get_content_id(self) -> str:
149
- """Fetches the content id from the data file if it exists, otherwise an empty string."""
150
- content_id = ""
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, "")
151
116
 
152
- filename = self.data_path
153
- if os.path.exists(filename):
154
- with open(filename) as file:
155
- content_id = json.load(file)[self.ID_KEY]
156
117
  return content_id
157
118
 
158
119
  def _read_notebook(self) -> NotebookNode:
@@ -181,9 +142,9 @@ class MarkdownSyncer:
181
142
  def _get_content(self) -> str:
182
143
  content = ""
183
144
  match self._content_file_type:
184
- case FileType.MARKDOWN:
145
+ case ContentType.MARKDOWN:
185
146
  content = self._get_content_from_markdown_file()
186
- case FileType.NOTEBOOK:
147
+ case ContentType.NOTEBOOK:
187
148
  content = self._get_content_from_notebook_file()
188
149
  return content
189
150
 
@@ -195,7 +156,7 @@ class MarkdownSyncer:
195
156
  "markdown": self._get_content(),
196
157
  }
197
158
 
198
- def _send_request(self) -> str:
159
+ def sync_content(self) -> Response:
199
160
  """Sends the request to the CMS endpoint and returns the content id from the response."""
200
161
  response = self._request_handler.send_request(
201
162
  url=self._post_url, data=self._request_data()
@@ -217,11 +178,6 @@ class MarkdownSyncer:
217
178
  f"Response from the CMS does not contain a valid content id: {result}"
218
179
  )
219
180
  content_id: str = result
220
-
221
- return content_id
222
-
223
- def sync_content(self) -> str:
224
- """Sends the markdown content to the CMS endpoint and stores the id from the response."""
225
- content_id = self._send_request()
226
181
  self._save_content_id(content_id)
227
- return content_id
182
+
183
+ return response
@@ -0,0 +1,56 @@
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ssb-pubmd
3
- Version: 0.0.15
3
+ Version: 0.0.17
4
4
  Summary: SSB Pubmd
5
5
  License: MIT
6
6
  Author: Olav Landsverk
@@ -14,8 +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)
19
+ Requires-Dist: platformdirs (>=4.3.8,<5.0.0)
18
20
  Requires-Dist: playwright (>=1.51.0,<2.0.0)
21
+ Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
19
22
  Requires-Dist: requests (>=2.32.3,<3.0.0)
20
23
  Requires-Dist: types-requests (>=2.32.0.20250306,<3.0.0.0)
21
24
  Project-URL: Changelog, https://github.com/statisticsnorway/ssb-pubmd/releases
@@ -0,0 +1,13 @@
1
+ ssb_pubmd/__init__.py,sha256=hWhlVS_FDH9yq7fBdXwggg5opjZwf7gIKY6ZyJ81Y4w,176
2
+ ssb_pubmd/__main__.py,sha256=xmxZouWkly1m4tXnxFiKWvsypzKuK2n8VnlgJlv40-s,5052
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.17.dist-info/LICENSE,sha256=tF5bnYv09fgH5ph9t1EpH1MGrVOGTQeswL4dzVeZ_ak,1073
10
+ ssb_pubmd-0.0.17.dist-info/METADATA,sha256=mtiBeaHoKkJSXlzivwJ2GU-0L8CIz-rGdaS1hvOI69s,4284
11
+ ssb_pubmd-0.0.17.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
12
+ ssb_pubmd-0.0.17.dist-info/entry_points.txt,sha256=1_NfsiOfqTg948JWXYPwi4QtDk90KHkNn1CQtye8rJ0,48
13
+ ssb_pubmd-0.0.17.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- ssb_pubmd/__init__.py,sha256=hWhlVS_FDH9yq7fBdXwggg5opjZwf7gIKY6ZyJ81Y4w,176
2
- ssb_pubmd/__main__.py,sha256=0Fo6rSeca4uQegIqD5At7I-3smeDwSoM4uG0YvWjK3U,2970
3
- ssb_pubmd/browser_request_handler.py,sha256=mxUQD2_5pd3wy4z5im4XvdzRCknFjGDNEulflXWWWNs,2900
4
- ssb_pubmd/markdown_syncer.py,sha256=LjwNap3OktkOqt8q-U1tBq2Q79Omygul2-qOBXSYaZQ,7651
5
- ssb_pubmd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- ssb_pubmd-0.0.15.dist-info/LICENSE,sha256=tF5bnYv09fgH5ph9t1EpH1MGrVOGTQeswL4dzVeZ_ak,1073
7
- ssb_pubmd-0.0.15.dist-info/METADATA,sha256=xTENomJbJ5X-frnB4C5z2h89_MPeVYbFsNMPuke9Zyg,4139
8
- ssb_pubmd-0.0.15.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
9
- ssb_pubmd-0.0.15.dist-info/entry_points.txt,sha256=1_NfsiOfqTg948JWXYPwi4QtDk90KHkNn1CQtye8rJ0,48
10
- ssb_pubmd-0.0.15.dist-info/RECORD,,