outnote 1.1.0__tar.gz → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: outnote
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Headless Microsoft 365 client - mail, calendar, Teams, OneDrive, SharePoint and OneNote over Graph, no app registration.
5
5
  Keywords: microsoft,microsoft-365,microsoft-graph,graph-api,outlook,onenote,onedrive,sharepoint,teams,mail,calendar,headless,no-app-registration,foci,entra
6
6
  Author: PianoNic
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
21
21
  Classifier: Topic :: Internet :: WWW/HTTP
22
22
  Classifier: Topic :: Office/Business
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Requires-Dist: ms-entrance>=1.3
24
+ Requires-Dist: ms-entrance>=1.4
25
25
  Requires-Dist: pypdf>=4 ; extra == 'pdf'
26
26
  Requires-Python: >=3.10
27
27
  Project-URL: Homepage, https://github.com/PianoNic/OutNote
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "outnote"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Headless Microsoft 365 client - mail, calendar, Teams, OneDrive, SharePoint and OneNote over Graph, no app registration."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -43,7 +43,7 @@ classifiers = [
43
43
  "Topic :: Office/Business",
44
44
  "Topic :: Software Development :: Libraries :: Python Modules",
45
45
  ]
46
- dependencies = ["ms-entrance>=1.3"]
46
+ dependencies = ["ms-entrance>=1.4"]
47
47
 
48
48
  [[project.authors]]
49
49
  name = "PianoNic"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "outnote"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Headless Microsoft 365 client - mail, calendar, Teams, OneDrive, SharePoint and OneNote over Graph, no app registration."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -47,7 +47,7 @@ classifiers = [
47
47
  "Topic :: Software Development :: Libraries :: Python Modules",
48
48
  ]
49
49
  dependencies = [
50
- "ms-entrance>=1.3",
50
+ "ms-entrance>=1.4",
51
51
  ]
52
52
 
53
53
  [project.optional-dependencies]
@@ -0,0 +1,58 @@
1
+ """Headless Microsoft 365 sign-in - a thin wrapper over `entrance.foci_login`.
2
+
3
+ All the auth mechanics (PKCE sign-in, device code, the FOCI family refresh
4
+ token, the token broker and its persistence) live in `entrance`; OutNote only
5
+ supplies the Microsoft 365 specifics - the sign-in client and the Office client
6
+ used to validate a stored token.
7
+ """
8
+
9
+ from entrance import OFFICEHOME, foci_login
10
+
11
+ from ._base import C_OFFICE
12
+ from ._graph import OutNote
13
+
14
+ __all__ = ["authenticate", "OFFICEHOME"]
15
+
16
+
17
+ def authenticate(
18
+ username=None,
19
+ password=None,
20
+ *,
21
+ tenant="common",
22
+ totp_secret=None,
23
+ totp_code=None,
24
+ cookies=".ms_session.json",
25
+ frt_store=".ms_frt",
26
+ device=False,
27
+ open_browser=False,
28
+ on_prompt=None,
29
+ **login_kwargs,
30
+ ):
31
+ """Sign in and return an `OutNote` client wired to a token broker.
32
+
33
+ No app registration or admin consent needed. If `frt_store` holds a live
34
+ refresh token, no sign-in happens at all. Otherwise pick how to sign in:
35
+ * default - headless (password + TOTP, or a seeded cookie jar).
36
+ * `device=True` - the device code flow: a short code shown on the terminal
37
+ that you enter at a URL, no browser redirect or copy-paste (the Office
38
+ client is used, which supports it and brokers to every M365 service).
39
+ * `interactive=True` / `"always"` - finish in a browser.
40
+ MFA: pass `totp_secret` (Base32) or `totp_code`. Extra kwargs pass through
41
+ to `entrance.login`.
42
+ """
43
+ broker = foci_login(
44
+ username,
45
+ password,
46
+ client_id=C_OFFICE if device else OFFICEHOME,
47
+ tenant=tenant,
48
+ totp_secret=totp_secret,
49
+ totp_code=totp_code,
50
+ cookies=cookies,
51
+ frt_store=frt_store,
52
+ probe_client=C_OFFICE,
53
+ device=device,
54
+ open_browser=open_browser,
55
+ on_prompt=on_prompt,
56
+ **login_kwargs,
57
+ )
58
+ return OutNote(broker)
@@ -10,17 +10,31 @@ import json
10
10
  import os
11
11
  import sys
12
12
 
13
+ from entrance import DeviceCodeError, LoginFailed, TokenError
14
+
13
15
  from ._auth import authenticate
16
+ from ._base import GraphError
14
17
  from ._text import message_text
15
18
 
19
+ _SIGNIN_HELP = """Not signed in. Do one of:
20
+ - sign in (easiest): outnote signin
21
+ shows a short code to enter at https://login.microsoft.com/device
22
+ - or set credentials (first run only):
23
+ OUTNOTE_USER / OUTNOTE_PASSWORD / OUTNOTE_TOTP (set on Windows, export on POSIX)
24
+ Either way a refresh token is saved (OUTNOTE_FRT, default .ms_frt), so later runs
25
+ need neither. `outnote signin --browser` signs in in a browser instead."""
26
+
16
27
 
17
- def _client():
28
+ def _client(device=False, browser=False):
18
29
  return authenticate(
19
30
  os.environ.get("OUTNOTE_USER"),
20
31
  os.environ.get("OUTNOTE_PASSWORD"),
21
32
  totp_secret=os.environ.get("OUTNOTE_TOTP"),
22
33
  cookies=os.environ.get("OUTNOTE_COOKIES", ".ms_session.json"),
23
34
  frt_store=os.environ.get("OUTNOTE_FRT", ".ms_frt"),
35
+ device=device,
36
+ open_browser=device, # also open the device page automatically
37
+ interactive="always" if browser else False,
24
38
  )
25
39
 
26
40
 
@@ -75,6 +89,12 @@ def main(argv=None):
75
89
  ap = argparse.ArgumentParser(prog="outnote", description="Microsoft 365 over Graph.")
76
90
  ap.add_argument("--text", action="store_true", help="readable output instead of JSON")
77
91
  sub = ap.add_subparsers(dest="cmd", required=True)
92
+ p = sub.add_parser(
93
+ "signin",
94
+ aliases=["login"],
95
+ help="sign in (device code by default, or --browser) and save the session",
96
+ )
97
+ p.add_argument("--browser", action="store_true", help="sign in in a browser instead")
78
98
  sub.add_parser("me")
79
99
  p = sub.add_parser("mail")
80
100
  p.add_argument("--folder", default="inbox")
@@ -100,8 +120,44 @@ def main(argv=None):
100
120
  p = sub.add_parser("get", help="raw GET of any Graph path")
101
121
  p.add_argument("path")
102
122
  a = ap.parse_args(argv)
123
+ sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
124
+ signin = a.cmd in ("signin", "login")
125
+ browser = getattr(a, "browser", False)
126
+
127
+ try:
128
+ # `signin` defaults to the device code flow; --browser switches to a browser.
129
+ note = _client(device=signin and not browser, browser=browser)
130
+ except LoginFailed:
131
+ print(_SIGNIN_HELP, file=sys.stderr)
132
+ return 2
133
+ except TokenError as e:
134
+ print(f"Sign-in failed: {e}", file=sys.stderr)
135
+ if e.error in ("invalid_grant", "interaction_required"):
136
+ print("\n" + _SIGNIN_HELP, file=sys.stderr)
137
+ return 2
138
+ except DeviceCodeError as e:
139
+ print(f"Sign-in failed: {e}", file=sys.stderr)
140
+ return 2
103
141
 
104
- note = _client()
142
+ if signin:
143
+ me = note.me()
144
+ store = os.environ.get("OUTNOTE_FRT", ".ms_frt")
145
+ print(f"Signed in as {me.get('displayName')} <{me.get('userPrincipalName')}>.")
146
+ print(f"Session saved to {store} - later commands reuse it, no sign-in needed.")
147
+ return 0
148
+
149
+ try:
150
+ data = _run(a, note)
151
+ except GraphError as e:
152
+ print(f"Graph error: {e}", file=sys.stderr)
153
+ return 1
154
+
155
+ out = _text(a.cmd, data, note) if a.text else json.dumps(data, indent=2, ensure_ascii=False)
156
+ print(out)
157
+ return 0
158
+
159
+
160
+ def _run(a, note):
105
161
  if a.cmd == "me":
106
162
  data = note.me()
107
163
  elif a.cmd == "mail":
@@ -120,11 +176,7 @@ def main(argv=None):
120
176
  data = note.files(a.path)
121
177
  else:
122
178
  data = note.get(a.path)
123
-
124
- out = _text(a.cmd, data, note) if a.text else json.dumps(data, indent=2, ensure_ascii=False)
125
- sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
126
- print(out)
127
- return 0
179
+ return data
128
180
 
129
181
 
130
182
  if __name__ == "__main__":
@@ -1,48 +0,0 @@
1
- """Headless Microsoft 365 sign-in - a thin wrapper over `entrance.foci_login`.
2
-
3
- All the auth mechanics (PKCE sign-in, the FOCI family refresh token, the token
4
- broker and its persistence) live in `entrance`; OutNote only supplies the
5
- Microsoft 365 specifics - the OfficeHome sign-in client (entrance's default)
6
- and the Office client used to validate a stored token.
7
- """
8
-
9
- from entrance import OFFICEHOME, foci_login
10
-
11
- from ._base import C_OFFICE
12
- from ._graph import OutNote
13
-
14
- __all__ = ["authenticate", "OFFICEHOME"]
15
-
16
-
17
- def authenticate(
18
- username=None,
19
- password=None,
20
- *,
21
- tenant="common",
22
- totp_secret=None,
23
- totp_code=None,
24
- cookies=".ms_session.json",
25
- frt_store=".ms_frt",
26
- **login_kwargs,
27
- ):
28
- """Sign in and return an `OutNote` client wired to a token broker.
29
-
30
- No app registration or admin consent needed. If `frt_store` holds a live
31
- refresh token, no sign-in happens at all; otherwise `entrance` signs in and
32
- the new token is saved there (pass `frt_store=None` to disable).
33
- `username`/`password` are optional once `cookies` holds a live session.
34
- MFA: pass `totp_secret` (Base32) or `totp_code`; or `interactive=True` to
35
- finish in a browser. Extra kwargs pass through to `entrance.login`.
36
- """
37
- broker = foci_login(
38
- username,
39
- password,
40
- tenant=tenant,
41
- totp_secret=totp_secret,
42
- totp_code=totp_code,
43
- cookies=cookies,
44
- frt_store=frt_store,
45
- probe_client=C_OFFICE,
46
- **login_kwargs,
47
- )
48
- return OutNote(broker)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes