markdown-to-confluence 0.4.4__py3-none-any.whl → 0.4.6__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.
md2conf/api.py CHANGED
@@ -11,6 +11,8 @@ import enum
11
11
  import io
12
12
  import logging
13
13
  import mimetypes
14
+ import ssl
15
+ import sys
14
16
  import typing
15
17
  from dataclasses import dataclass
16
18
  from pathlib import Path
@@ -19,15 +21,28 @@ from typing import Any, Optional, TypeVar
19
21
  from urllib.parse import urlencode, urlparse, urlunparse
20
22
 
21
23
  import requests
24
+ from requests.adapters import HTTPAdapter
22
25
  from strong_typing.core import JsonType
23
26
  from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
24
27
 
28
+ from .environment import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
29
+ from .extra import override
25
30
  from .metadata import ConfluenceSiteMetadata
26
- from .properties import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
31
+
32
+ if sys.version_info >= (3, 10):
33
+ import truststore
34
+ else:
35
+ import certifi
27
36
 
28
37
  T = TypeVar("T")
29
38
 
39
+ mimetypes.add_type("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", strict=True)
30
40
  mimetypes.add_type("text/vnd.mermaid", ".mmd", strict=True)
41
+ mimetypes.add_type("application/vnd.oasis.opendocument.presentation", ".odp", strict=True)
42
+ mimetypes.add_type("application/vnd.oasis.opendocument.spreadsheet", ".ods", strict=True)
43
+ mimetypes.add_type("application/vnd.oasis.opendocument.text", ".odt", strict=True)
44
+ mimetypes.add_type("application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx", strict=True)
45
+ mimetypes.add_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", strict=True)
31
46
 
32
47
 
33
48
  def _json_to_object(
@@ -330,9 +345,33 @@ class ConfluenceUpdateAttachmentRequest:
330
345
  version: ConfluenceContentVersion
331
346
 
332
347
 
348
+ class TruststoreAdapter(HTTPAdapter):
349
+ """
350
+ Provides a general-case interface for HTTPS sessions to connect to HTTPS URLs.
351
+
352
+ This class implements the Transport Adapter interface in the Python library `requests`.
353
+
354
+ This class will usually be created by the :class:`requests.Session` class under the covers.
355
+ """
356
+
357
+ @override
358
+ def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **pool_kwargs: Any) -> None:
359
+ """
360
+ Adapts the pool manager to use the provided SSL context instead of the default.
361
+ """
362
+
363
+ if sys.version_info >= (3, 10):
364
+ ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
365
+ else:
366
+ ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
367
+ ctx.check_hostname = True
368
+ ctx.verify_mode = ssl.CERT_REQUIRED
369
+ super().init_poolmanager(connections, maxsize, block, ssl_context=ctx, **pool_kwargs) # type: ignore[no-untyped-call]
370
+
371
+
333
372
  class ConfluenceAPI:
334
373
  """
335
- Represents an active connection to a Confluence server.
374
+ Encapsulates operations that can be invoked via the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/v2/).
336
375
  """
337
376
 
338
377
  properties: ConfluenceConnectionProperties
@@ -342,7 +381,13 @@ class ConfluenceAPI:
342
381
  self.properties = properties or ConfluenceConnectionProperties()
343
382
 
344
383
  def __enter__(self) -> "ConfluenceSession":
384
+ """
385
+ Opens a connection to a Confluence server.
386
+ """
387
+
345
388
  session = requests.Session()
389
+ session.mount("https://", TruststoreAdapter())
390
+
346
391
  if self.properties.user_name:
347
392
  session.auth = (self.properties.user_name, self.properties.api_key)
348
393
  else:
@@ -366,6 +411,10 @@ class ConfluenceAPI:
366
411
  exc_val: Optional[BaseException],
367
412
  exc_tb: Optional[TracebackType],
368
413
  ) -> None:
414
+ """
415
+ Closes an open connection.
416
+ """
417
+
369
418
  if self.session is not None:
370
419
  self.session.close()
371
420
  self.session = None
@@ -373,7 +422,7 @@ class ConfluenceAPI:
373
422
 
374
423
  class ConfluenceSession:
375
424
  """
376
- Information about an open session to a Confluence server.
425
+ Represents an active connection to a Confluence server.
377
426
  """
378
427
 
379
428
  session: requests.Session
@@ -412,8 +461,31 @@ class ConfluenceSession:
412
461
  if not base_path:
413
462
  raise ArgumentError("Confluence base path not specified and cannot be inferred")
414
463
  self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
464
+
415
465
  if not api_url:
416
- self.api_url = f"https://{self.site.domain}{self.site.base_path}"
466
+ LOGGER.info("Discovering Confluence REST API URL")
467
+ try:
468
+ # obtain cloud ID to build URL for access with scoped token
469
+ response = self.session.get(f"https://{self.site.domain}/_edge/tenant_info", headers={"Accept": "application/json"}, verify=True)
470
+ if response.text:
471
+ LOGGER.debug("Received HTTP payload:\n%s", response.text)
472
+ response.raise_for_status()
473
+ cloud_id = response.json()["cloudId"]
474
+
475
+ # try next-generation REST API URL
476
+ LOGGER.info("Probing scoped Confluence REST API URL")
477
+ self.api_url = f"https://api.atlassian.com/ex/confluence/{cloud_id}/"
478
+ url = self._build_url(ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"})
479
+ response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
480
+ if response.text:
481
+ LOGGER.debug("Received HTTP payload:\n%s", response.text)
482
+ response.raise_for_status()
483
+
484
+ LOGGER.info("Configured scoped Confluence REST API URL: %s", self.api_url)
485
+ except requests.exceptions.HTTPError:
486
+ # fall back to classic REST API URL
487
+ self.api_url = f"https://{self.site.domain}{self.site.base_path}"
488
+ LOGGER.info("Configured classic Confluence REST API URL: %s", self.api_url)
417
489
 
418
490
  def close(self) -> None:
419
491
  self.session.close()
@@ -448,7 +520,7 @@ class ConfluenceSession:
448
520
  "Executes an HTTP request via Confluence API."
449
521
 
450
522
  url = self._build_url(version, path, query)
451
- response = self.session.get(url, headers={"Accept": "application/json"})
523
+ response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
452
524
  if response.text:
453
525
  LOGGER.debug("Received HTTP payload:\n%s", response.text)
454
526
  response.raise_for_status()
@@ -460,7 +532,7 @@ class ConfluenceSession:
460
532
  items: list[JsonType] = []
461
533
  url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
462
534
  while True:
463
- response = self.session.get(url, headers={"Accept": "application/json"})
535
+ response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
464
536
  response.raise_for_status()
465
537
 
466
538
  payload = typing.cast(dict[str, JsonType], response.json())
@@ -496,22 +568,16 @@ class ConfluenceSession:
496
568
  "Creates a new object via Confluence REST API."
497
569
 
498
570
  url, headers, data = self._build_request(version, path, body, response_type)
499
- response = self.session.post(
500
- url,
501
- data=data,
502
- headers=headers,
503
- )
571
+ response = self.session.post(url, data=data, headers=headers, verify=True)
572
+ response.raise_for_status()
504
573
  return response_cast(response_type, response)
505
574
 
506
575
  def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T:
507
576
  "Updates an existing object via Confluence REST API."
508
577
 
509
578
  url, headers, data = self._build_request(version, path, body, response_type)
510
- response = self.session.put(
511
- url,
512
- data=data,
513
- headers=headers,
514
- )
579
+ response = self.session.put(url, data=data, headers=headers, verify=True)
580
+ response.raise_for_status()
515
581
  return response_cast(response_type, response)
516
582
 
517
583
  def space_id_to_key(self, id: str) -> str:
@@ -681,6 +747,7 @@ class ConfluenceSession:
681
747
  "X-Atlassian-Token": "no-check",
682
748
  "Accept": "application/json",
683
749
  },
750
+ verify=True,
684
751
  )
685
752
  elif raw_data is not None:
686
753
  LOGGER.info("Uploading raw data: %s", attachment_name)
@@ -708,6 +775,7 @@ class ConfluenceSession:
708
775
  "X-Atlassian-Token": "no-check",
709
776
  "Accept": "application/json",
710
777
  },
778
+ verify=True,
711
779
  )
712
780
  else:
713
781
  raise NotImplementedError("parameter match not exhaustive")
@@ -869,6 +937,7 @@ class ConfluenceSession:
869
937
  "Content-Type": "application/json; charset=utf-8",
870
938
  "Accept": "application/json",
871
939
  },
940
+ verify=True,
872
941
  )
873
942
  response.raise_for_status()
874
943
  return _json_to_object(ConfluencePage, response.json())
@@ -886,7 +955,7 @@ class ConfluenceSession:
886
955
  # move to trash
887
956
  url = self._build_url(ConfluenceVersion.VERSION_2, path)
888
957
  LOGGER.info("Moving page to trash: %s", page_id)
889
- response = self.session.delete(url)
958
+ response = self.session.delete(url, verify=True)
890
959
  response.raise_for_status()
891
960
 
892
961
  if purge:
@@ -894,7 +963,7 @@ class ConfluenceSession:
894
963
  query = {"purge": "true"}
895
964
  url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
896
965
  LOGGER.info("Permanently deleting page: %s", page_id)
897
- response = self.session.delete(url)
966
+ response = self.session.delete(url, verify=True)
898
967
  response.raise_for_status()
899
968
 
900
969
  def page_exists(
@@ -929,6 +998,7 @@ class ConfluenceSession:
929
998
  "Content-Type": "application/json; charset=utf-8",
930
999
  "Accept": "application/json",
931
1000
  },
1001
+ verify=True,
932
1002
  )
933
1003
  response.raise_for_status()
934
1004
  data = typing.cast(dict[str, JsonType], response.json())
@@ -993,7 +1063,7 @@ class ConfluenceSession:
993
1063
  query = {"name": label.name}
994
1064
 
995
1065
  url = self._build_url(ConfluenceVersion.VERSION_1, path, query)
996
- response = self.session.delete(url)
1066
+ response = self.session.delete(url, verify=True)
997
1067
  if response.text:
998
1068
  LOGGER.debug("Received HTTP payload:\n%s", response.text)
999
1069
  response.raise_for_status()
@@ -1052,7 +1122,7 @@ class ConfluenceSession:
1052
1122
 
1053
1123
  path = f"/pages/{page_id}/properties/{property_id}"
1054
1124
  url = self._build_url(ConfluenceVersion.VERSION_2, path)
1055
- response = self.session.delete(url)
1125
+ response = self.session.delete(url, verify=True)
1056
1126
  response.raise_for_status()
1057
1127
 
1058
1128
  def update_content_property_for_page(