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.
- onenote_enterprise-1.0.0/PKG-INFO +57 -0
- onenote_enterprise-1.0.0/README.md +46 -0
- onenote_enterprise-1.0.0/onenote_enterprise.egg-info/PKG-INFO +57 -0
- onenote_enterprise-1.0.0/onenote_enterprise.egg-info/SOURCES.txt +11 -0
- onenote_enterprise-1.0.0/onenote_enterprise.egg-info/dependency_links.txt +1 -0
- onenote_enterprise-1.0.0/onenote_enterprise.egg-info/requires.txt +3 -0
- onenote_enterprise-1.0.0/onenote_enterprise.egg-info/top_level.txt +1 -0
- onenote_enterprise-1.0.0/pyproject.toml +20 -0
- onenote_enterprise-1.0.0/setup.cfg +4 -0
- onenote_enterprise-1.0.0/tests/test_client.py +115 -0
- onenote_enterprise-1.0.0/tests/test_content_parser.py +138 -0
- onenote_enterprise-1.0.0/tests/test_permissions.py +79 -0
- onenote_enterprise-1.0.0/tests/test_url_parser.py +65 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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,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()
|