ssb-pubmd 0.0.12__py3-none-any.whl → 0.0.14__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """SSB Pubmd."""
2
2
 
3
- from .browser_context import BrowserRequestContext as BrowserContext
3
+ from .browser_request_handler import BrowserRequestHandler
4
4
  from .markdown_syncer import MarkdownSyncer
5
5
 
6
- __all__ = ["BrowserContext", "MarkdownSyncer"]
6
+ __all__ = ["BrowserRequestHandler", "MarkdownSyncer"]
ssb_pubmd/__main__.py CHANGED
@@ -1,52 +1,107 @@
1
1
  """Command-line interface."""
2
2
 
3
+ import json
3
4
  import os
5
+ from dataclasses import asdict
6
+ from dataclasses import dataclass
4
7
 
5
8
  import click
6
9
 
7
- from ssb_pubmd.browser_context import BrowserRequestContext as RequestContext
10
+ from ssb_pubmd.browser_request_handler import BrowserRequestHandler
11
+ from ssb_pubmd.browser_request_handler import CreateContextMethod
8
12
  from ssb_pubmd.markdown_syncer import MarkdownSyncer
9
13
 
14
+ BASE_DIR = os.path.join(os.path.expanduser("~"), ".pubmd")
15
+ os.makedirs(BASE_DIR, exist_ok=True)
10
16
 
11
- @click.group()
12
- def cli() -> None:
13
- """Command-line interface for the ssb_pubmd package."""
14
- pass
17
+ CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
18
+ BROWSER_CONTEXT_PATH = os.path.join(BASE_DIR, "browser_context.json")
19
+
20
+
21
+ @dataclass
22
+ class Config:
23
+ """Handles the user configuration."""
24
+
25
+ login_url: str = ""
26
+ post_url: str = ""
27
+
28
+ @classmethod
29
+ def load(cls, path: str) -> "Config":
30
+ """Loads the configuration from a file."""
31
+ with open(path) as f:
32
+ data = json.load(f)
33
+ return cls(**data)
34
+
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)
39
+
40
+
41
+ @click.command()
42
+ def config() -> None:
43
+ """Set the login and post URL for the CMS."""
44
+ config_file = CONFIG_PATH
45
+
46
+ login_url = click.prompt("Enter the login URL", type=str)
47
+ post_url = click.prompt("Enter the post URL", type=str)
48
+
49
+ config = Config(login_url=login_url, post_url=post_url)
50
+
51
+ config.save(config_file)
52
+
53
+ click.echo(f"\nConfiguration stored in:\n{config_file}")
15
54
 
16
55
 
17
56
  @click.command()
18
57
  def login() -> None:
19
- """Login to the server."""
20
- login_url = os.getenv("PUBMD_LOGIN_URL", "")
21
- request_context = RequestContext()
22
- print(login_url)
23
- storage_state_file, storage_state = request_context.create_new(login_url)
24
- click.echo(
25
- f"The following browser context object is now stored in {storage_state_file}:"
26
- )
27
- click.echo(storage_state)
58
+ """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)
64
+
65
+ method = CreateContextMethod.FROM_LOGIN
66
+ with request_handler.new_context(method=method):
67
+ click.echo("Logging in...")
68
+
69
+ click.echo(f"\nBrowser context stored in:\n{browser_context_file}")
28
70
 
29
71
 
30
72
  @click.command()
31
73
  @click.argument("content_file_path", type=click.Path())
32
74
  def sync(content_file_path: str) -> None:
33
- """Sync the content."""
34
- post_url = os.getenv("PUBMD_POST_URL", "")
35
- request_context = RequestContext()
36
- request_context.recreate_from_file()
75
+ """Sync a markdown or notebook file to the CMS."""
76
+ config_file = CONFIG_PATH
77
+ browser_context_file = BROWSER_CONTEXT_PATH
37
78
 
38
- syncer = MarkdownSyncer(post_url=post_url, request_context=request_context)
39
- syncer.content_file_path = content_file_path
79
+ request_handler = BrowserRequestHandler(browser_context_file)
40
80
 
41
- content_id = syncer.sync_content()
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
+ )
86
+
87
+ syncer.content_file_path = content_file_path
88
+ content_id = syncer.sync_content()
42
89
 
43
90
  click.echo(
44
- f"File '{click.format_filename(content_file_path)}' synced to CMS with content ID: {content_id}"
91
+ f"File '{click.format_filename(browser_context_file)}' synced to CMS with content ID: {content_id}."
45
92
  )
93
+ click.echo(f"Response data saved to '{click.format_filename(syncer.data_path)}'.")
94
+
95
+
96
+ @click.group()
97
+ def cli() -> None:
98
+ """Pubmd - a tool to sync markdown and notebook files to a CMS."""
99
+ pass
46
100
 
47
101
 
102
+ cli.add_command(config)
48
103
  cli.add_command(login)
49
104
  cli.add_command(sync)
50
105
 
51
106
  if __name__ == "__main__":
52
- cli() # pragma: no cover
107
+ cli()
@@ -0,0 +1,85 @@
1
+ import os
2
+ from collections.abc import Iterator
3
+ from contextlib import contextmanager
4
+ from enum import Enum
5
+
6
+ from playwright.sync_api import BrowserContext
7
+ from playwright.sync_api import sync_playwright
8
+
9
+ from .markdown_syncer import Response
10
+
11
+
12
+ class CreateContextMethod(Enum):
13
+ """The method used to create the browser context.
14
+
15
+ Can be either from a file containing the context data,
16
+ or from a login popup window.
17
+ """
18
+
19
+ FROM_FILE = "from_file"
20
+ FROM_LOGIN = "from_login"
21
+
22
+
23
+ class BrowserRequestHandler:
24
+ """This class is used to create a logged in browser context from which to send requests."""
25
+
26
+ def __init__(self, context_file_path: str, login_url: str = "") -> None:
27
+ """Initializes an empty browser context object."""
28
+ self._context_file_path: str = os.path.abspath(context_file_path)
29
+ self._login_url: str = login_url
30
+ self._context: BrowserContext | None = None
31
+
32
+ @contextmanager
33
+ def new_context(
34
+ self, method: CreateContextMethod = CreateContextMethod.FROM_FILE
35
+ ) -> Iterator[BrowserContext]:
36
+ """Wrapper around playwright's context manager.
37
+
38
+ The default is to create a new context from a file.
39
+ If `from_file` is set False, a new context is created through a browser popup with user login,
40
+ and the context is saved to a file.
41
+ """
42
+ with sync_playwright() as playwright:
43
+ browser = playwright.chromium.launch(headless=False)
44
+ match method:
45
+ case CreateContextMethod.FROM_FILE:
46
+ self._context = browser.new_context(
47
+ storage_state=self._context_file_path
48
+ )
49
+ case CreateContextMethod.FROM_LOGIN:
50
+ self._context = browser.new_context()
51
+ login_page = self._context.new_page()
52
+ login_page.goto(self._login_url)
53
+ login_page.wait_for_event("close", timeout=0)
54
+ self._context.storage_state(path=self._context_file_path)
55
+ yield self._context
56
+ self._context.close()
57
+ browser.close()
58
+
59
+ def send_request(
60
+ self,
61
+ url: str,
62
+ headers: dict[str, str] | None = None,
63
+ data: dict[str, str] | None = None,
64
+ ) -> Response:
65
+ """Sends a request to the specified url, optionally with headers and data, within the browser context."""
66
+ if self._context is None:
67
+ raise ValueError("Browser context has not been created.")
68
+
69
+ api_response = self._context.request.post(
70
+ url,
71
+ data=data,
72
+ )
73
+
74
+ try:
75
+ body = api_response.json()
76
+ body = dict(body)
77
+ except Exception:
78
+ body = None
79
+
80
+ response = Response(
81
+ status_code=api_response.status,
82
+ body=body,
83
+ )
84
+
85
+ return response
@@ -18,8 +18,8 @@ class Response:
18
18
  body: dict[str, Any] | None = None
19
19
 
20
20
 
21
- class RequestContext(Protocol):
22
- """Interface for the context in which a request is sent.
21
+ class RequestHandler(Protocol):
22
+ """Interface for the handling how the request is sent.
23
23
 
24
24
  Implementing classes may handle authentication, sessions, etc.
25
25
  """
@@ -34,11 +34,11 @@ class RequestContext(Protocol):
34
34
  ...
35
35
 
36
36
 
37
- class BasicRequestContext:
38
- """Basic, unauthenticated request context."""
37
+ class BasicRequestHandler:
38
+ """Basic, unauthenticated request handler."""
39
39
 
40
40
  def __init__(self) -> None:
41
- """Initializes the basic request context."""
41
+ """Initializes the basic request handler."""
42
42
  pass
43
43
 
44
44
  def send_request(
@@ -91,10 +91,10 @@ class MarkdownSyncer:
91
91
 
92
92
  ID_KEY = "_id"
93
93
 
94
- def __init__(self, post_url: str, request_context: RequestContext) -> None:
94
+ def __init__(self, post_url: str, request_handler: RequestHandler) -> None:
95
95
  """Creates a markdown syncer instance that connects to the CMS through the post url."""
96
96
  self._post_url: str = post_url
97
- self._context: RequestContext = request_context
97
+ self._request_handler: RequestHandler = request_handler
98
98
  self._content_file_path: str = ""
99
99
  self._content_file_type: FileType = FileType.MARKDOWN
100
100
 
@@ -132,7 +132,7 @@ class MarkdownSyncer:
132
132
  @property
133
133
  def data_path(self) -> str:
134
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] + ".json"
135
+ return os.path.splitext(self.content_file_path)[0] + "-PUBMD.json"
136
136
 
137
137
  @property
138
138
  def display_name(self) -> str:
@@ -197,7 +197,7 @@ class MarkdownSyncer:
197
197
 
198
198
  def _send_request(self) -> str:
199
199
  """Sends the request to the CMS endpoint and returns the content id from the response."""
200
- response = self._context.send_request(
200
+ response = self._request_handler.send_request(
201
201
  url=self._post_url, data=self._request_data()
202
202
  )
203
203
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ssb-pubmd
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Summary: SSB Pubmd
5
5
  License: MIT
6
6
  Author: Olav Landsverk
@@ -51,47 +51,29 @@ Description-Content-Type: text/markdown
51
51
  [black]: https://github.com/psf/black
52
52
  [poetry]: https://python-poetry.org/
53
53
 
54
- ## Features
55
-
56
- - Supports logging in through a popup browser window.
57
-
58
- ## Requirements
59
-
60
- - This library uses [playwright](https://github.com/microsoft/playwright-python) to create a logged in browser context. This requires installing a [browser binary](https://playwright.dev/python/docs/browsers#install-browsers) and necessary [system dependencies](https://playwright.dev/python/docs/browsers#install-system-dependencies).
61
54
 
62
55
  ## Installation
63
56
 
64
- You can install _SSB Pubmd_ via [pip] from [PyPI]:
57
+ Installation with pip:
65
58
 
66
59
  ```console
67
60
  pip install ssb-pubmd
68
61
  ```
69
62
 
70
- ## Usage
71
-
72
- First set environment variables:
63
+ If you need to create a logged-in browser context, you will also need to install a [Playwright browser](https://playwright.dev/python/docs/browsers#install-browsers):
73
64
 
74
65
  ```console
75
- export PUBMD_LOGIN_URL=<https://www.example.com/login>
76
- export PUBMD_POST_URL=<https://www.example.com/post>
66
+ playwright install --with-deps chromium
77
67
  ```
78
68
 
79
- To log in, run:
80
-
81
- ```console
82
- pubmd login
83
- ```
84
-
85
- Close the popup browser window when you are logged in.
69
+ ## Usage
86
70
 
87
- To synchronize markdown content to the CMS server, run:
71
+ Run the main command in a terminal to see available subcommands with documentation:
88
72
 
89
73
  ```console
90
- pubmd sync <file>
74
+ pubmd
91
75
  ```
92
76
 
93
- `<file>` should be an absolute or relative path, and the allowed extensions are `.ipynb` and `md`.
94
-
95
77
  ## Contributing
96
78
 
97
79
  Contributions are very welcome.
@@ -0,0 +1,10 @@
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.14.dist-info/LICENSE,sha256=tF5bnYv09fgH5ph9t1EpH1MGrVOGTQeswL4dzVeZ_ak,1073
7
+ ssb_pubmd-0.0.14.dist-info/METADATA,sha256=y_vWYiUtKeVEr7yHV_4D6pVi3hDJ5SPKKuRFgbJzzkI,4139
8
+ ssb_pubmd-0.0.14.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
9
+ ssb_pubmd-0.0.14.dist-info/entry_points.txt,sha256=1_NfsiOfqTg948JWXYPwi4QtDk90KHkNn1CQtye8rJ0,48
10
+ ssb_pubmd-0.0.14.dist-info/RECORD,,
@@ -1,71 +0,0 @@
1
- from playwright.sync_api import BrowserContext
2
- from playwright.sync_api import StorageState
3
- from playwright.sync_api import sync_playwright
4
-
5
- from .markdown_syncer import Response
6
-
7
- BROWSER_CONTEXT_FILE = "browser_context.json"
8
-
9
-
10
- class BrowserRequestContext:
11
- """This class is used to create a logged in browser context from which to send requests."""
12
-
13
- def __init__(self) -> None:
14
- """Initializes an empty browser context object."""
15
- self._storage_state_path: str = BROWSER_CONTEXT_FILE
16
- self._context: BrowserContext | None = None
17
-
18
- def create_new(self, login_url: str) -> tuple[str, StorageState]:
19
- """Creates a browser context by opening a login page and waiting for it to be closed by user.
20
-
21
- This function also saves the browser context to a file for later use.
22
- """
23
- playwright = sync_playwright().start()
24
- browser = playwright.chromium.launch(headless=False)
25
-
26
- self._context = browser.new_context()
27
- login_page = self._context.new_page()
28
-
29
- login_page.goto(login_url)
30
- login_page.wait_for_event("close", timeout=0)
31
-
32
- storage_state = self._context.storage_state(path=self._storage_state_path)
33
-
34
- return self._storage_state_path, storage_state
35
-
36
- def recreate_from_file(self) -> BrowserContext:
37
- """Recreates a browser context object from a file."""
38
- playwright = sync_playwright().start()
39
- browser = playwright.chromium.launch(headless=False)
40
-
41
- self._context = browser.new_context(storage_state=self._storage_state_path)
42
-
43
- return self._context
44
-
45
- def send_request(
46
- self,
47
- url: str,
48
- headers: dict[str, str] | None = None,
49
- data: dict[str, str] | None = None,
50
- ) -> Response:
51
- """Sends a request to the specified url, optionally with headers and data, within the browser context."""
52
- if self._context is None:
53
- raise ValueError("Browser context has not been created.")
54
-
55
- api_response = self._context.request.post(
56
- url,
57
- params=data,
58
- )
59
-
60
- try:
61
- body = api_response.json()
62
- body = dict(body)
63
- except Exception:
64
- body = None
65
-
66
- response = Response(
67
- status_code=api_response.status,
68
- body=body,
69
- )
70
-
71
- return response
@@ -1,10 +0,0 @@
1
- ssb_pubmd/__init__.py,sha256=WD-U43QQTQIRSRgTHSnEjcG5MuH4T_CPFHPXols2NLk,179
2
- ssb_pubmd/__main__.py,sha256=WdiICEorWrTbM1NDIkG-97Gg49IV5Pm_MBkQHr3Scec,1364
3
- ssb_pubmd/browser_context.py,sha256=JXDlvUJZ-NYLQlE0_MRAiuRxGrMVBfvm5Rt2QX7dYRA,2344
4
- ssb_pubmd/markdown_syncer.py,sha256=RHP3bHIGPRLichrleygOS2WIVN1KOlYPehbrcenJMRk,7631
5
- ssb_pubmd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- ssb_pubmd-0.0.12.dist-info/LICENSE,sha256=tF5bnYv09fgH5ph9t1EpH1MGrVOGTQeswL4dzVeZ_ak,1073
7
- ssb_pubmd-0.0.12.dist-info/METADATA,sha256=6YfXG5dXzW-2xU-Cdbtik5-ophdBRngUsSbBJ_DVYkw,4707
8
- ssb_pubmd-0.0.12.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
9
- ssb_pubmd-0.0.12.dist-info/entry_points.txt,sha256=1_NfsiOfqTg948JWXYPwi4QtDk90KHkNn1CQtye8rJ0,48
10
- ssb_pubmd-0.0.12.dist-info/RECORD,,