onenote-enterprise 1.0.0__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.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: onenote_enterprise
3
+ Version: 1.0.0
4
+ Summary: Production-grade Microsoft OneNote connector via Graph API
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.31.0
9
+ Requires-Dist: beautifulsoup4>=4.12.0
10
+ Requires-Dist: lxml>=4.9.0
11
+
12
+ # onenote_connector
13
+
14
+ Production-grade Python module for extracting content from
15
+ Microsoft OneNote via Microsoft Graph API.
16
+
17
+ ## Installation
18
+ pip install -r requirements.txt
19
+
20
+ ## Quick Start
21
+ from onenote_connector import OneNoteConnector
22
+
23
+ connector = OneNoteConnector(access_token="your_bearer_token")
24
+ result = connector.get_content_from_url("https://...sharepoint.com/...")
25
+
26
+ ## Methods
27
+ - get_content_from_url(url) → list[dict]
28
+ - get_all_notebooks() → list[dict]
29
+ - get_notebook_by_name(name) → list[dict]
30
+
31
+ ## Output Fields Per Page
32
+ | Field | Type | Description |
33
+ |---|---|---|
34
+ | id | str | Graph API page ID |
35
+ | title | str | Page title |
36
+ | content | str | Plain text content |
37
+ | paragraphs | list | Text paragraphs |
38
+ | headings | list | h1-h6 headings |
39
+ | blocks | list | Ordered text blocks with type and heading level when applicable |
40
+ | tables | list | Tables with rows |
41
+ | lists | list | ul and ol lists |
42
+ | tags | list | OneNote tags |
43
+ | hyperlinks | list | Links |
44
+ | attachments | list | File attachments |
45
+ | loop_components | list | Loop / Fluid placeholders pointing to separate .loop files |
46
+ | images | list | Base64 images |
47
+ | permissions | list | Who has access |
48
+
49
+ ## Error Handling
50
+ | Exception | When |
51
+ |---|---|
52
+ | AuthenticationError | Token expired or invalid |
53
+ | InvalidURLError | URL cannot be parsed |
54
+ | NotFoundError | Notebook/page not found |
55
+ | RateLimitError | Graph API throttling |
56
+ | ParseError | HTML parsing failed |
57
+ | NetworkError | Connection timeout |
@@ -0,0 +1,46 @@
1
+ # onenote_connector
2
+
3
+ Production-grade Python module for extracting content from
4
+ Microsoft OneNote via Microsoft Graph API.
5
+
6
+ ## Installation
7
+ pip install -r requirements.txt
8
+
9
+ ## Quick Start
10
+ from onenote_connector import OneNoteConnector
11
+
12
+ connector = OneNoteConnector(access_token="your_bearer_token")
13
+ result = connector.get_content_from_url("https://...sharepoint.com/...")
14
+
15
+ ## Methods
16
+ - get_content_from_url(url) → list[dict]
17
+ - get_all_notebooks() → list[dict]
18
+ - get_notebook_by_name(name) → list[dict]
19
+
20
+ ## Output Fields Per Page
21
+ | Field | Type | Description |
22
+ |---|---|---|
23
+ | id | str | Graph API page ID |
24
+ | title | str | Page title |
25
+ | content | str | Plain text content |
26
+ | paragraphs | list | Text paragraphs |
27
+ | headings | list | h1-h6 headings |
28
+ | blocks | list | Ordered text blocks with type and heading level when applicable |
29
+ | tables | list | Tables with rows |
30
+ | lists | list | ul and ol lists |
31
+ | tags | list | OneNote tags |
32
+ | hyperlinks | list | Links |
33
+ | attachments | list | File attachments |
34
+ | loop_components | list | Loop / Fluid placeholders pointing to separate .loop files |
35
+ | images | list | Base64 images |
36
+ | permissions | list | Who has access |
37
+
38
+ ## Error Handling
39
+ | Exception | When |
40
+ |---|---|
41
+ | AuthenticationError | Token expired or invalid |
42
+ | InvalidURLError | URL cannot be parsed |
43
+ | NotFoundError | Notebook/page not found |
44
+ | RateLimitError | Graph API throttling |
45
+ | ParseError | HTML parsing failed |
46
+ | NetworkError | Connection timeout |
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: onenote_enterprise
3
+ Version: 1.0.0
4
+ Summary: Production-grade Microsoft OneNote connector via Graph API
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.31.0
9
+ Requires-Dist: beautifulsoup4>=4.12.0
10
+ Requires-Dist: lxml>=4.9.0
11
+
12
+ # onenote_connector
13
+
14
+ Production-grade Python module for extracting content from
15
+ Microsoft OneNote via Microsoft Graph API.
16
+
17
+ ## Installation
18
+ pip install -r requirements.txt
19
+
20
+ ## Quick Start
21
+ from onenote_connector import OneNoteConnector
22
+
23
+ connector = OneNoteConnector(access_token="your_bearer_token")
24
+ result = connector.get_content_from_url("https://...sharepoint.com/...")
25
+
26
+ ## Methods
27
+ - get_content_from_url(url) → list[dict]
28
+ - get_all_notebooks() → list[dict]
29
+ - get_notebook_by_name(name) → list[dict]
30
+
31
+ ## Output Fields Per Page
32
+ | Field | Type | Description |
33
+ |---|---|---|
34
+ | id | str | Graph API page ID |
35
+ | title | str | Page title |
36
+ | content | str | Plain text content |
37
+ | paragraphs | list | Text paragraphs |
38
+ | headings | list | h1-h6 headings |
39
+ | blocks | list | Ordered text blocks with type and heading level when applicable |
40
+ | tables | list | Tables with rows |
41
+ | lists | list | ul and ol lists |
42
+ | tags | list | OneNote tags |
43
+ | hyperlinks | list | Links |
44
+ | attachments | list | File attachments |
45
+ | loop_components | list | Loop / Fluid placeholders pointing to separate .loop files |
46
+ | images | list | Base64 images |
47
+ | permissions | list | Who has access |
48
+
49
+ ## Error Handling
50
+ | Exception | When |
51
+ |---|---|
52
+ | AuthenticationError | Token expired or invalid |
53
+ | InvalidURLError | URL cannot be parsed |
54
+ | NotFoundError | Notebook/page not found |
55
+ | RateLimitError | Graph API throttling |
56
+ | ParseError | HTML parsing failed |
57
+ | NetworkError | Connection timeout |
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ onenote_enterprise.egg-info/PKG-INFO
4
+ onenote_enterprise.egg-info/SOURCES.txt
5
+ onenote_enterprise.egg-info/dependency_links.txt
6
+ onenote_enterprise.egg-info/requires.txt
7
+ onenote_enterprise.egg-info/top_level.txt
8
+ tests/test_client.py
9
+ tests/test_content_parser.py
10
+ tests/test_permissions.py
11
+ tests/test_url_parser.py
@@ -0,0 +1,3 @@
1
+ requests>=2.31.0
2
+ beautifulsoup4>=4.12.0
3
+ lxml>=4.9.0
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "onenote_enterprise"
7
+ version = "1.0.0"
8
+ description = "Production-grade Microsoft OneNote connector via Graph API"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ dependencies = [
13
+ "requests>=2.31.0",
14
+ "beautifulsoup4>=4.12.0",
15
+ "lxml>=4.9.0",
16
+ ]
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["."]
20
+ include = ["onenote_enterprise*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,115 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+ import requests
4
+
5
+ from onenote_connector.client import GraphClient
6
+ from onenote_connector.exceptions import (
7
+ AuthenticationError,
8
+ NotFoundError,
9
+ RateLimitError,
10
+ NetworkError,
11
+ )
12
+
13
+
14
+ class TestGraphClient(unittest.TestCase):
15
+
16
+ def setUp(self) -> None:
17
+ self.valid_token = (
18
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
19
+ "eyJzdWIiOiIxMjM0NTY3ODkwIn0."
20
+ "dQw4w9WgXcQ"
21
+ )
22
+
23
+ def test_init_valid_token(self) -> None:
24
+ client = GraphClient(self.valid_token)
25
+ self.assertIsNotNone(client)
26
+
27
+ def test_init_empty_token_raises_error(self) -> None:
28
+ with self.assertRaises(AuthenticationError):
29
+ GraphClient("")
30
+
31
+ def test_init_none_token_raises_error(self) -> None:
32
+ with self.assertRaises(AuthenticationError):
33
+ GraphClient(None) # type: ignore
34
+
35
+ @patch("onenote_connector.client.requests.Session.get")
36
+ def test_get_returns_json(self, mock_get: MagicMock) -> None:
37
+ mock_response = MagicMock()
38
+ mock_response.status_code = 200
39
+ mock_response.json.return_value = {"key": "value"}
40
+ mock_get.return_value = mock_response
41
+
42
+ client = GraphClient(self.valid_token)
43
+ result = client.get("/me")
44
+ self.assertEqual(result, {"key": "value"})
45
+
46
+ @patch("onenote_connector.client.requests.Session.get")
47
+ def test_get_404_raises_not_found(self, mock_get: MagicMock) -> None:
48
+ mock_response = MagicMock()
49
+ mock_response.status_code = 404
50
+ mock_response.text = "Not Found"
51
+ mock_get.return_value = mock_response
52
+
53
+ client = GraphClient(self.valid_token)
54
+ with self.assertRaises(NotFoundError):
55
+ client.get("/me/onenote/notebooks/nonexistent")
56
+
57
+ @patch("onenote_connector.client.requests.Session.get")
58
+ def test_get_429_raises_rate_limit(self, mock_get: MagicMock) -> None:
59
+ mock_response = MagicMock()
60
+ mock_response.status_code = 429
61
+ mock_response.text = "Too Many Requests"
62
+ mock_response.headers = {"Retry-After": "30"}
63
+ mock_get.return_value = mock_response
64
+
65
+ client = GraphClient(self.valid_token)
66
+ with self.assertRaises(RateLimitError) as ctx:
67
+ client.get("/me/onenote/notebooks")
68
+ self.assertEqual(ctx.exception.retry_after, 30)
69
+
70
+ @patch("onenote_connector.client.requests.Session.get")
71
+ def test_get_401_raises_auth_error(self, mock_get: MagicMock) -> None:
72
+ mock_response = MagicMock()
73
+ mock_response.status_code = 401
74
+ mock_response.text = "Unauthorized"
75
+ mock_get.return_value = mock_response
76
+
77
+ client = GraphClient(self.valid_token)
78
+ with self.assertRaises(AuthenticationError):
79
+ client.get("/me")
80
+
81
+ @patch("onenote_connector.client.requests.Session.get")
82
+ def test_timeout_raises_network_error(self, mock_get: MagicMock) -> None:
83
+ mock_get.side_effect = requests.exceptions.Timeout("Connection timed out")
84
+
85
+ client = GraphClient(self.valid_token)
86
+ with self.assertRaises(NetworkError):
87
+ client.get("/me")
88
+
89
+ @patch("onenote_connector.client.GraphClient.get")
90
+ def test_get_paginated_follows_next_link(self, mock_get: MagicMock) -> None:
91
+ mock_get.side_effect = [
92
+ {"value": [{"id": "1"}], "@odata.nextLink": "https://graph.microsoft.com/v1.0/next"},
93
+ {"value": [{"id": "2"}], "@odata.nextLink": None},
94
+ ]
95
+
96
+ client = GraphClient(self.valid_token)
97
+ result = client.get_paginated("/me/onenote/notebooks")
98
+ self.assertEqual(len(result), 2)
99
+ self.assertEqual(result[0]["id"], "1")
100
+ self.assertEqual(result[1]["id"], "2")
101
+
102
+ @patch("onenote_connector.client.requests.Session.get")
103
+ def test_get_raw_returns_bytes(self, mock_get: MagicMock) -> None:
104
+ mock_response = MagicMock()
105
+ mock_response.status_code = 200
106
+ mock_response.content = b"\x89PNG\r\n\x1a\n"
107
+ mock_get.return_value = mock_response
108
+
109
+ client = GraphClient(self.valid_token)
110
+ result = client.get_raw("https://example.com/image.png")
111
+ self.assertEqual(result, b"\x89PNG\r\n\x1a\n")
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
@@ -0,0 +1,138 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+ from onenote_connector.parsers.content_parser import ContentParser
4
+
5
+
6
+ class TestContentParser(unittest.TestCase):
7
+
8
+ def setUp(self) -> None:
9
+ self.access_token = "test_token"
10
+ self.parser = ContentParser(self.access_token)
11
+
12
+ def test_empty_html_returns_empty_fields(self) -> None:
13
+ mock_client = MagicMock()
14
+ result = self.parser.parse("", mock_client)
15
+ self.assertEqual(result["content"], "")
16
+ self.assertEqual(result["paragraphs"], [])
17
+ self.assertEqual(result["headings"], [])
18
+ self.assertEqual(result["blocks"], [])
19
+ self.assertEqual(result["tables"], [])
20
+ self.assertEqual(result["lists"], [])
21
+ self.assertEqual(result["tags"], [])
22
+ self.assertEqual(result["loop_components"], [])
23
+
24
+ def test_none_html_returns_empty_fields(self) -> None:
25
+ mock_client = MagicMock()
26
+ result = self.parser.parse(None, mock_client) # type: ignore
27
+ self.assertEqual(result["content"], "")
28
+
29
+ def test_paragraphs_extracted(self) -> None:
30
+ mock_client = MagicMock()
31
+ html = "<html><body><p>First paragraph</p><p>Second paragraph</p></body></html>"
32
+ result = self.parser.parse(html, mock_client)
33
+ self.assertEqual(len(result["paragraphs"]), 2)
34
+ self.assertIn("First paragraph", result["paragraphs"])
35
+ self.assertIn("Second paragraph", result["paragraphs"])
36
+
37
+ def test_headings_extracted(self) -> None:
38
+ mock_client = MagicMock()
39
+ html = "<html><body><h1>Title</h1><h2>Subtitle</h2></body></html>"
40
+ result = self.parser.parse(html, mock_client)
41
+ self.assertEqual(len(result["headings"]), 2)
42
+ self.assertEqual(result["headings"][0]["level"], "h1")
43
+ self.assertEqual(result["headings"][0]["text"], "Title")
44
+ self.assertEqual(result["headings"][1]["level"], "h2")
45
+ self.assertEqual(result["headings"][1]["text"], "Subtitle")
46
+ self.assertIn("Title", result["content"])
47
+ self.assertIn("Subtitle", result["content"])
48
+
49
+ def test_content_keeps_paragraph_and_heading_text_together(self) -> None:
50
+ mock_client = MagicMock()
51
+ html = """
52
+ <html>
53
+ <body>
54
+ <h1>Project Notes</h1>
55
+ <p>First paragraph</p>
56
+ <p>Second paragraph</p>
57
+ </body>
58
+ </html>
59
+ """
60
+ result = self.parser.parse(html, mock_client)
61
+ self.assertEqual(result["paragraphs"], ["First paragraph", "Second paragraph"])
62
+ self.assertEqual(result["headings"], [{"level": "h1", "text": "Project Notes"}])
63
+ self.assertEqual(
64
+ result["content"],
65
+ "Project Notes\nFirst paragraph\nSecond paragraph",
66
+ )
67
+ self.assertEqual(
68
+ result["blocks"],
69
+ [
70
+ {"type": "heading", "level": "h1", "text": "Project Notes"},
71
+ {"type": "paragraph", "text": "First paragraph"},
72
+ {"type": "paragraph", "text": "Second paragraph"},
73
+ ],
74
+ )
75
+
76
+ def test_lists_extracted(self) -> None:
77
+ mock_client = MagicMock()
78
+ html = """
79
+ <html>
80
+ <body>
81
+ <ul><li>Item 1</li><li>Item 2</li></ul>
82
+ <ol><li>Step 1</li><li>Step 2</li></ol>
83
+ </body>
84
+ </html>
85
+ """
86
+ result = self.parser.parse(html, mock_client)
87
+ self.assertEqual(len(result["lists"]), 2)
88
+ self.assertEqual(result["lists"][0]["type"], "ul")
89
+ self.assertEqual(result["lists"][1]["type"], "ol")
90
+
91
+ def test_hyperlinks_extracted(self) -> None:
92
+ mock_client = MagicMock()
93
+ html = '<html><body><a href="https://example.com">Example</a></body></html>'
94
+ result = self.parser.parse(html, mock_client)
95
+ self.assertEqual(len(result["hyperlinks"]), 1)
96
+ self.assertEqual(result["hyperlinks"][0]["href"], "https://example.com")
97
+ self.assertEqual(result["hyperlinks"][0]["text"], "Example")
98
+
99
+ def test_tags_extracted(self) -> None:
100
+ mock_client = MagicMock()
101
+ html = '<html><body><p data-tag="to-do">Submit timesheet</p></body></html>'
102
+ result = self.parser.parse(html, mock_client)
103
+ self.assertEqual(len(result["tags"]), 1)
104
+ self.assertEqual(result["tags"][0]["tag_type"], "to-do")
105
+ self.assertEqual(result["tags"][0]["completed"], False)
106
+ self.assertEqual(result["tags"][0]["text"], "Submit timesheet")
107
+
108
+ def test_completed_tag(self) -> None:
109
+ mock_client = MagicMock()
110
+ html = '<html><body><p data-tag="to-do:completed">Done</p></body></html>'
111
+ result = self.parser.parse(html, mock_client)
112
+ self.assertEqual(len(result["tags"]), 1)
113
+ self.assertEqual(result["tags"][0]["tag_type"], "to-do")
114
+ self.assertEqual(result["tags"][0]["completed"], True)
115
+
116
+ def test_attachments_extracted(self) -> None:
117
+ mock_client = MagicMock()
118
+ html = '<html><body><object data-attachment="report.pdf" type="application/pdf"></object></body></html>'
119
+ result = self.parser.parse(html, mock_client)
120
+ self.assertEqual(len(result["attachments"]), 1)
121
+ self.assertEqual(result["attachments"][0]["filename"], "report.pdf")
122
+ self.assertEqual(result["attachments"][0]["mime_type"], "application/pdf")
123
+
124
+ def test_loop_component_detected(self) -> None:
125
+ mock_client = MagicMock()
126
+ html = (
127
+ '<html><body>'
128
+ '<object data-app-id="Loop" data-src="https://my.sharepoint.com/sites/demo/file.loop"></object>'
129
+ '</body></html>'
130
+ )
131
+ result = self.parser.parse(html, mock_client)
132
+ self.assertEqual(len(result["loop_components"]), 1)
133
+ self.assertEqual(result["loop_components"][0]["type"], "loop_component")
134
+ self.assertTrue(result["loop_components"][0]["src"].endswith(".loop"))
135
+
136
+
137
+ if __name__ == "__main__":
138
+ unittest.main()
@@ -0,0 +1,79 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock
3
+
4
+ from onenote_connector.exporters import JsonExporter
5
+ from onenote_connector.models import Notebook, Page, Section
6
+ from onenote_connector.permissions import PermissionResolver
7
+
8
+
9
+ class TestPermissions(unittest.TestCase):
10
+ def test_permission_resolver_maps_graph_response(self) -> None:
11
+ client = MagicMock()
12
+ client.get.side_effect = [
13
+ {"parentReference": {"driveId": "drive-1"}, "id": "item-1"},
14
+ {
15
+ "value": [
16
+ {
17
+ "roles": ["owner"],
18
+ "grantedToV2": {
19
+ "user": {
20
+ "displayName": "Jhon DOE",
21
+ "email": "jhon@example.com",
22
+ }
23
+ },
24
+ }
25
+ ]
26
+ },
27
+ ]
28
+
29
+ resolver = PermissionResolver(client)
30
+ result = resolver.get_permissions("Quick Notes")
31
+
32
+ self.assertEqual(
33
+ result,
34
+ [
35
+ {
36
+ "principal_name": "Jhon DOE",
37
+ "principal_email": "jhon@example.com",
38
+ "principal_type": "user",
39
+ "roles": ["owner"],
40
+ "source": "graph",
41
+ }
42
+ ],
43
+ )
44
+
45
+ def test_json_exporter_includes_top_level_permissions(self) -> None:
46
+ exporter = JsonExporter()
47
+ notebook = Notebook(id="nb-1", display_name="Quick Notes")
48
+ section = Section(id="sec-1", display_name="Section 1")
49
+ page = Page(id="page-1", title="Page 1")
50
+
51
+ data = exporter.to_list(
52
+ notebook=notebook,
53
+ sections=[section],
54
+ pages_by_section={"sec-1": [page]},
55
+ parsed_content={"page-1": exporter._empty_page_content()},
56
+ permissions=[
57
+ {
58
+ "principal_email": "",
59
+ "principal_name": "Jhon DOE",
60
+ "principal_type": "direct_user",
61
+ "roles": ["owner"],
62
+ "source": "graph",
63
+ }
64
+ ],
65
+ )
66
+
67
+ self.assertEqual(
68
+ data[0]["sections"][0]["pages"][0]["notebook_permissions"][0]["principal_name"],
69
+ "Jhon DOE",
70
+ )
71
+ self.assertEqual(
72
+ data[0]["sections"][0]["pages"][0]["notebook_permissions"][0]["principal_type"],
73
+ "direct_user",
74
+ )
75
+ self.assertEqual(data[0]["sections"][0]["pages"][0]["blocks"], [])
76
+
77
+
78
+ if __name__ == "__main__":
79
+ unittest.main()
@@ -0,0 +1,65 @@
1
+ import unittest
2
+ from onenote_connector.discovery.url_parser import URLParser
3
+ from onenote_connector.exceptions import InvalidURLError
4
+
5
+
6
+ class TestURLParser(unittest.TestCase):
7
+
8
+ def setUp(self) -> None:
9
+ self.parser = URLParser()
10
+
11
+ def test_sharepoint_url(self) -> None:
12
+ url = (
13
+ "https://binaryrepublik-my.sharepoint.com/personal/"
14
+ "mayur_panchal_binaryrepublik_com/_layouts/15/Doc.aspx"
15
+ "?sourcedoc={0f150add-404d-4206-bbe4-1b666cee0f15}"
16
+ "&action=edit"
17
+ "&wd=target%28Timesheet.one%7Cd02e35d8-d8d3-4e5b-9b92-eef1320cce7f"
18
+ "%2FTimesheet%20From%2011-6-26%7Cd543efe0-4463-4307-8aeb-0d87e0c49109%2F%29"
19
+ "&wdorigin=NavigationUrl"
20
+ )
21
+ result = self.parser.parse(url)
22
+
23
+ self.assertEqual(result.hostname, "binaryrepublik-my.sharepoint.com")
24
+ self.assertEqual(result.user_path, "mayur_panchal_binaryrepublik_com")
25
+ self.assertEqual(result.sourcedoc_guid, "0f150add-404d-4206-bbe4-1b666cee0f15")
26
+ self.assertEqual(result.notebook_name, "Timesheet")
27
+ self.assertEqual(result.section_name, "Timesheet.one")
28
+ self.assertEqual(result.section_guid, "d02e35d8-d8d3-4e5b-9b92-eef1320cce7f")
29
+ self.assertEqual(result.page_title, "Timesheet From 11-6-26")
30
+ self.assertEqual(result.page_guid, "d543efe0-4463-4307-8aeb-0d87e0c49109")
31
+
32
+ def test_graph_api_url(self) -> None:
33
+ url = "https://graph.microsoft.com/v1.0/users/user123/onenote/notebooks/notebook456"
34
+ result = self.parser.parse(url)
35
+
36
+ self.assertEqual(result.hostname, "graph.microsoft.com")
37
+ self.assertEqual(result.user_path, "user123")
38
+ self.assertEqual(result.notebook_name, "")
39
+
40
+ def test_empty_url_raises_error(self) -> None:
41
+ with self.assertRaises(InvalidURLError):
42
+ self.parser.parse("")
43
+
44
+ def test_none_url_raises_error(self) -> None:
45
+ with self.assertRaises(InvalidURLError):
46
+ self.parser.parse(None) # type: ignore
47
+
48
+ def test_invalid_url_format(self) -> None:
49
+ url = "not-a-valid-url"
50
+ result = self.parser.parse(url)
51
+ self.assertEqual(result.hostname, "")
52
+ self.assertEqual(result.sourcedoc_guid, "")
53
+
54
+ def test_sharepoint_sites_url(self) -> None:
55
+ url = (
56
+ "https://contoso.sharepoint.com/sites/MySite/"
57
+ "_layouts/15/Doc.aspx?sourcedoc={guid123}&action=edit"
58
+ )
59
+ result = self.parser.parse(url)
60
+ self.assertEqual(result.user_path, "MySite")
61
+ self.assertEqual(result.sourcedoc_guid, "guid123")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ unittest.main()