owa-tools 0.1.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.
Files changed (111) hide show
  1. owa_tools-0.1.0/LICENSE +21 -0
  2. owa_tools-0.1.0/PKG-INFO +114 -0
  3. owa_tools-0.1.0/README.md +97 -0
  4. owa_tools-0.1.0/owa/__init__.py +3 -0
  5. owa_tools-0.1.0/owa/__main__.py +7 -0
  6. owa_tools-0.1.0/owa/cli.py +193 -0
  7. owa_tools-0.1.0/owa_cal/__init__.py +10 -0
  8. owa_tools-0.1.0/owa_cal/__main__.py +6 -0
  9. owa_tools-0.1.0/owa_cal/api.py +55 -0
  10. owa_tools-0.1.0/owa_cal/auth.py +40 -0
  11. owa_tools-0.1.0/owa_cal/cli.py +985 -0
  12. owa_tools-0.1.0/owa_cal/config.py +47 -0
  13. owa_tools-0.1.0/owa_cal/dates.py +68 -0
  14. owa_tools-0.1.0/owa_cal/events.py +262 -0
  15. owa_tools-0.1.0/owa_cal/format.py +37 -0
  16. owa_tools-0.1.0/owa_cal/ics.py +302 -0
  17. owa_tools-0.1.0/owa_cal/profiles.py +94 -0
  18. owa_tools-0.1.0/owa_core/__init__.py +5 -0
  19. owa_tools-0.1.0/owa_core/auth.py +220 -0
  20. owa_tools-0.1.0/owa_core/config.py +101 -0
  21. owa_tools-0.1.0/owa_core/errors.py +121 -0
  22. owa_tools-0.1.0/owa_core/format.py +32 -0
  23. owa_tools-0.1.0/owa_core/http.py +213 -0
  24. owa_tools-0.1.0/owa_core/jwt.py +63 -0
  25. owa_tools-0.1.0/owa_core/modes.py +140 -0
  26. owa_tools-0.1.0/owa_core/schema.py +76 -0
  27. owa_tools-0.1.0/owa_core/secrets.py +67 -0
  28. owa_tools-0.1.0/owa_core/tty.py +30 -0
  29. owa_tools-0.1.0/owa_core/version.py +45 -0
  30. owa_tools-0.1.0/owa_doctor/__init__.py +9 -0
  31. owa_tools-0.1.0/owa_doctor/__main__.py +6 -0
  32. owa_tools-0.1.0/owa_doctor/cli.py +189 -0
  33. owa_tools-0.1.0/owa_doctor/format.py +65 -0
  34. owa_tools-0.1.0/owa_doctor/probe.py +135 -0
  35. owa_tools-0.1.0/owa_drive/__init__.py +9 -0
  36. owa_tools-0.1.0/owa_drive/__main__.py +6 -0
  37. owa_tools-0.1.0/owa_drive/api.py +88 -0
  38. owa_tools-0.1.0/owa_drive/auth.py +34 -0
  39. owa_tools-0.1.0/owa_drive/cli.py +449 -0
  40. owa_tools-0.1.0/owa_drive/config.py +41 -0
  41. owa_tools-0.1.0/owa_drive/format.py +61 -0
  42. owa_tools-0.1.0/owa_drive/items.py +39 -0
  43. owa_tools-0.1.0/owa_drive/paths.py +82 -0
  44. owa_tools-0.1.0/owa_graph/__init__.py +10 -0
  45. owa_tools-0.1.0/owa_graph/__main__.py +7 -0
  46. owa_tools-0.1.0/owa_graph/api.py +155 -0
  47. owa_tools-0.1.0/owa_graph/auth.py +80 -0
  48. owa_tools-0.1.0/owa_graph/cli.py +740 -0
  49. owa_tools-0.1.0/owa_graph/config.py +47 -0
  50. owa_tools-0.1.0/owa_graph/ctx.py +143 -0
  51. owa_tools-0.1.0/owa_graph/data/__init__.py +5 -0
  52. owa_tools-0.1.0/owa_graph/data/paths.json.gz +0 -0
  53. owa_tools-0.1.0/owa_graph/data/scopes.json +75 -0
  54. owa_tools-0.1.0/owa_graph/emit.py +134 -0
  55. owa_tools-0.1.0/owa_graph/format.py +419 -0
  56. owa_tools-0.1.0/owa_graph/paths.py +75 -0
  57. owa_tools-0.1.0/owa_graph/resources/__init__.py +67 -0
  58. owa_tools-0.1.0/owa_graph/resources/_argv.py +49 -0
  59. owa_tools-0.1.0/owa_graph/resources/calendar.py +118 -0
  60. owa_tools-0.1.0/owa_graph/resources/chats.py +34 -0
  61. owa_tools-0.1.0/owa_graph/resources/contacts.py +47 -0
  62. owa_tools-0.1.0/owa_graph/resources/directory.py +23 -0
  63. owa_tools-0.1.0/owa_graph/resources/files.py +101 -0
  64. owa_tools-0.1.0/owa_graph/resources/groups.py +45 -0
  65. owa_tools-0.1.0/owa_graph/resources/mail.py +133 -0
  66. owa_tools-0.1.0/owa_graph/resources/me.py +55 -0
  67. owa_tools-0.1.0/owa_graph/resources/planner.py +57 -0
  68. owa_tools-0.1.0/owa_graph/resources/presence.py +40 -0
  69. owa_tools-0.1.0/owa_graph/resources/sites.py +38 -0
  70. owa_tools-0.1.0/owa_graph/resources/teams.py +54 -0
  71. owa_tools-0.1.0/owa_graph/resources/todo.py +46 -0
  72. owa_tools-0.1.0/owa_graph/resources/users.py +58 -0
  73. owa_tools-0.1.0/owa_graph/scopes.py +94 -0
  74. owa_tools-0.1.0/owa_mail/__init__.py +10 -0
  75. owa_tools-0.1.0/owa_mail/__main__.py +6 -0
  76. owa_tools-0.1.0/owa_mail/api.py +55 -0
  77. owa_tools-0.1.0/owa_mail/auth.py +40 -0
  78. owa_tools-0.1.0/owa_mail/cli.py +747 -0
  79. owa_tools-0.1.0/owa_mail/config.py +42 -0
  80. owa_tools-0.1.0/owa_mail/dates.py +30 -0
  81. owa_tools-0.1.0/owa_mail/folders.py +63 -0
  82. owa_tools-0.1.0/owa_mail/format.py +93 -0
  83. owa_tools-0.1.0/owa_mail/messages.py +286 -0
  84. owa_tools-0.1.0/owa_mail/scheduled.py +50 -0
  85. owa_tools-0.1.0/owa_people/__init__.py +9 -0
  86. owa_tools-0.1.0/owa_people/__main__.py +6 -0
  87. owa_tools-0.1.0/owa_people/api.py +52 -0
  88. owa_tools-0.1.0/owa_people/auth.py +39 -0
  89. owa_tools-0.1.0/owa_people/cli.py +419 -0
  90. owa_tools-0.1.0/owa_people/config.py +42 -0
  91. owa_tools-0.1.0/owa_people/format.py +50 -0
  92. owa_tools-0.1.0/owa_people/people.py +51 -0
  93. owa_tools-0.1.0/owa_sched/__init__.py +9 -0
  94. owa_tools-0.1.0/owa_sched/__main__.py +6 -0
  95. owa_tools-0.1.0/owa_sched/api.py +37 -0
  96. owa_tools-0.1.0/owa_sched/auth.py +38 -0
  97. owa_tools-0.1.0/owa_sched/cli.py +436 -0
  98. owa_tools-0.1.0/owa_sched/config.py +53 -0
  99. owa_tools-0.1.0/owa_sched/dates.py +90 -0
  100. owa_tools-0.1.0/owa_sched/format.py +44 -0
  101. owa_tools-0.1.0/owa_sched/schedule.py +79 -0
  102. owa_tools-0.1.0/owa_tools.egg-info/PKG-INFO +114 -0
  103. owa_tools-0.1.0/owa_tools.egg-info/SOURCES.txt +109 -0
  104. owa_tools-0.1.0/owa_tools.egg-info/dependency_links.txt +1 -0
  105. owa_tools-0.1.0/owa_tools.egg-info/entry_points.txt +9 -0
  106. owa_tools-0.1.0/owa_tools.egg-info/requires.txt +7 -0
  107. owa_tools-0.1.0/owa_tools.egg-info/top_level.txt +9 -0
  108. owa_tools-0.1.0/pyproject.toml +88 -0
  109. owa_tools-0.1.0/setup.cfg +4 -0
  110. owa_tools-0.1.0/tests/test_agents_index.py +68 -0
  111. owa_tools-0.1.0/tests/test_architecture_contracts.py +91 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carl Joakim Damsleth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: owa-tools
3
+ Version: 0.1.0
4
+ Summary: Monorepo workspace for owa-piggy consumer CLIs
5
+ Author: Carl Joakim Damsleth
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Provides-Extra: dev
11
+ Requires-Dist: build>=1; extra == "dev"
12
+ Requires-Dist: pytest>=7; extra == "dev"
13
+ Requires-Dist: pytest-cov>=4; extra == "dev"
14
+ Requires-Dist: ruff>=0.4; extra == "dev"
15
+ Requires-Dist: twine>=5; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # owa-tools
19
+
20
+ Monorepo for the seven `owa-piggy` consumer CLIs: `owa-cal`, `owa-mail`, `owa-graph`, `owa-doctor`, `owa-people`, `owa-sched`, `owa-drive`. Plus the umbrella `owa` discovery binary.
21
+
22
+ The auth broker `owa-piggy` lives in its own repository. It is the only persistent-secret holder in the suite.
23
+
24
+ ## Status
25
+
26
+ `owa-tools` is currently unreleased and uses one suite version for all console scripts.
27
+
28
+ | CLI | Status |
29
+ |---|---|
30
+ | `owa-cal` | beta |
31
+ | `owa-mail` | beta |
32
+ | `owa-graph` | beta |
33
+ | `owa-doctor` | alpha |
34
+ | `owa-people` | alpha |
35
+ | `owa-sched` | alpha |
36
+ | `owa-drive` | alpha |
37
+ | `owa` | umbrella discovery binary |
38
+
39
+ ## Layout
40
+
41
+ ```
42
+ owa-tools/
43
+ ├── owa_cal/ calendar CRUD over Outlook REST
44
+ ├── owa_mail/ mail CRUD over Outlook REST
45
+ ├── owa_graph/ Microsoft Graph CLI (verb-first + 14 resource shortcut groups)
46
+ ├── owa_doctor/ health check across the suite
47
+ ├── owa_people/ people, directory, contacts (Graph)
48
+ ├── owa_sched/ free/busy and slot finding (Graph)
49
+ ├── owa_drive/ OneDrive CRUD (Graph)
50
+ ├── owa/ umbrella `owa` binary (list, schema, doctor, version)
51
+ ├── tools/ CI helpers (stdlib check)
52
+ ├── tests/ per-tool test suites
53
+ ├── completions/ bash, zsh, fish
54
+ └── docs/ per-tool docs
55
+ ```
56
+
57
+ ## Running
58
+
59
+ Local dev install:
60
+
61
+ ```bash
62
+ python3 -m venv .venv
63
+ .venv/bin/pip install -e .[dev]
64
+ .venv/bin/owa list
65
+ ```
66
+
67
+ Wheel build:
68
+
69
+ ```bash
70
+ .venv/bin/python -m build --wheel
71
+ ```
72
+
73
+ The wheel contains all eight console scripts (`owa`, `owa-cal`, `owa-mail`, `owa-graph`, `owa-doctor`, `owa-people`, `owa-sched`, `owa-drive`) and they all report the same `owa-tools` suite version.
74
+
75
+ Test suite:
76
+
77
+ ```bash
78
+ .venv/bin/python -m pytest
79
+ .venv/bin/coverage run --source=owa_core -m pytest -q
80
+ .venv/bin/coverage report --fail-under=95
81
+ .venv/bin/python -m pytest --cov --cov-fail-under=90
82
+ .venv/bin/python tools/check_stdlib_only.py
83
+ .venv/bin/python tools/check_no_secrets.py
84
+ .venv/bin/python tools/check_docs_sync.py
85
+ .venv/bin/python tools/check_artifacts.py dist/* # after build
86
+ .venv/bin/python tools/check_console_smoke.py # after build
87
+ ```
88
+
89
+ See `RELEASING.md` for the suite tag-and-publish flow.
90
+
91
+ ## More Docs
92
+
93
+ - `docs/security.md` defines the token, config, redaction, and live-test
94
+ boundaries.
95
+ - `docs/agent-integration.md` documents schema discovery, `--agent`, and
96
+ `--err-json`.
97
+ - `docs/profile-model.md` explains how `owa-tools` profile aliases map to
98
+ `owa-piggy` profiles.
99
+ - `docs/migrating-from-individual-installs.md` walks existing users from
100
+ the legacy per-tool installs to the `owa-tools` suite.
101
+
102
+ ## Conventions
103
+
104
+ - Stdlib only at runtime, except for the local suite packages and `owa-piggy`.
105
+ - JSON on stdout, logs on stderr, `--pretty` for humans.
106
+ - `--agent` wraps JSON-compatible output for automation; `--err-json` emits
107
+ structured stderr.
108
+ - Auth via `owa-piggy` (subprocess, JSON contract).
109
+ - For agents and contributors, start with `AGENTS.md`, then read the nearest
110
+ local `AGENTS.md` for the package or directory being edited.
111
+
112
+ ## License
113
+
114
+ MIT.
@@ -0,0 +1,97 @@
1
+ # owa-tools
2
+
3
+ Monorepo for the seven `owa-piggy` consumer CLIs: `owa-cal`, `owa-mail`, `owa-graph`, `owa-doctor`, `owa-people`, `owa-sched`, `owa-drive`. Plus the umbrella `owa` discovery binary.
4
+
5
+ The auth broker `owa-piggy` lives in its own repository. It is the only persistent-secret holder in the suite.
6
+
7
+ ## Status
8
+
9
+ `owa-tools` is currently unreleased and uses one suite version for all console scripts.
10
+
11
+ | CLI | Status |
12
+ |---|---|
13
+ | `owa-cal` | beta |
14
+ | `owa-mail` | beta |
15
+ | `owa-graph` | beta |
16
+ | `owa-doctor` | alpha |
17
+ | `owa-people` | alpha |
18
+ | `owa-sched` | alpha |
19
+ | `owa-drive` | alpha |
20
+ | `owa` | umbrella discovery binary |
21
+
22
+ ## Layout
23
+
24
+ ```
25
+ owa-tools/
26
+ ├── owa_cal/ calendar CRUD over Outlook REST
27
+ ├── owa_mail/ mail CRUD over Outlook REST
28
+ ├── owa_graph/ Microsoft Graph CLI (verb-first + 14 resource shortcut groups)
29
+ ├── owa_doctor/ health check across the suite
30
+ ├── owa_people/ people, directory, contacts (Graph)
31
+ ├── owa_sched/ free/busy and slot finding (Graph)
32
+ ├── owa_drive/ OneDrive CRUD (Graph)
33
+ ├── owa/ umbrella `owa` binary (list, schema, doctor, version)
34
+ ├── tools/ CI helpers (stdlib check)
35
+ ├── tests/ per-tool test suites
36
+ ├── completions/ bash, zsh, fish
37
+ └── docs/ per-tool docs
38
+ ```
39
+
40
+ ## Running
41
+
42
+ Local dev install:
43
+
44
+ ```bash
45
+ python3 -m venv .venv
46
+ .venv/bin/pip install -e .[dev]
47
+ .venv/bin/owa list
48
+ ```
49
+
50
+ Wheel build:
51
+
52
+ ```bash
53
+ .venv/bin/python -m build --wheel
54
+ ```
55
+
56
+ The wheel contains all eight console scripts (`owa`, `owa-cal`, `owa-mail`, `owa-graph`, `owa-doctor`, `owa-people`, `owa-sched`, `owa-drive`) and they all report the same `owa-tools` suite version.
57
+
58
+ Test suite:
59
+
60
+ ```bash
61
+ .venv/bin/python -m pytest
62
+ .venv/bin/coverage run --source=owa_core -m pytest -q
63
+ .venv/bin/coverage report --fail-under=95
64
+ .venv/bin/python -m pytest --cov --cov-fail-under=90
65
+ .venv/bin/python tools/check_stdlib_only.py
66
+ .venv/bin/python tools/check_no_secrets.py
67
+ .venv/bin/python tools/check_docs_sync.py
68
+ .venv/bin/python tools/check_artifacts.py dist/* # after build
69
+ .venv/bin/python tools/check_console_smoke.py # after build
70
+ ```
71
+
72
+ See `RELEASING.md` for the suite tag-and-publish flow.
73
+
74
+ ## More Docs
75
+
76
+ - `docs/security.md` defines the token, config, redaction, and live-test
77
+ boundaries.
78
+ - `docs/agent-integration.md` documents schema discovery, `--agent`, and
79
+ `--err-json`.
80
+ - `docs/profile-model.md` explains how `owa-tools` profile aliases map to
81
+ `owa-piggy` profiles.
82
+ - `docs/migrating-from-individual-installs.md` walks existing users from
83
+ the legacy per-tool installs to the `owa-tools` suite.
84
+
85
+ ## Conventions
86
+
87
+ - Stdlib only at runtime, except for the local suite packages and `owa-piggy`.
88
+ - JSON on stdout, logs on stderr, `--pretty` for humans.
89
+ - `--agent` wraps JSON-compatible output for automation; `--err-json` emits
90
+ structured stderr.
91
+ - Auth via `owa-piggy` (subprocess, JSON contract).
92
+ - For agents and contributors, start with `AGENTS.md`, then read the nearest
93
+ local `AGENTS.md` for the package or directory being edited.
94
+
95
+ ## License
96
+
97
+ MIT.
@@ -0,0 +1,3 @@
1
+ from owa_core.version import suite_version
2
+
3
+ __version__ = suite_version()
@@ -0,0 +1,7 @@
1
+ """Allow `python -m owa` for contract tests and local smoke checks."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ if __name__ == '__main__':
7
+ sys.exit(main())
@@ -0,0 +1,193 @@
1
+ """Umbrella `owa` binary. Discovery only; not a full dispatcher.
2
+
3
+ Subcommands:
4
+ owa list List installed consumer CLIs and their versions.
5
+ owa schema Aggregate schemas from each consumer CLI.
6
+ owa doctor Forward to `owa-doctor probe`.
7
+ owa version Print the umbrella version.
8
+
9
+ Real work lives in the consumer CLIs (owa-cal, owa-mail, ...).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from . import __version__
20
+
21
+ CONSUMERS = (
22
+ "owa-cal",
23
+ "owa-mail",
24
+ "owa-graph",
25
+ "owa-doctor",
26
+ "owa-people",
27
+ "owa-sched",
28
+ "owa-drive",
29
+ )
30
+
31
+
32
+ def _which(name: str) -> str | None:
33
+ for base in (
34
+ Path(sys.argv[0]).parent,
35
+ Path(sys.argv[0]).resolve().parent,
36
+ Path(sys.executable).parent,
37
+ Path(sys.executable).resolve().parent,
38
+ ):
39
+ candidate = base / name
40
+ if candidate.exists():
41
+ return str(candidate)
42
+ found = shutil.which(name)
43
+ if found:
44
+ return found
45
+ return None
46
+
47
+
48
+ def _version_of(binary: str) -> str | None:
49
+ """Best-effort version lookup. Tries `--version`, falls back to the
50
+ first line of `--help`. Returns None on failure.
51
+
52
+ Several owa-* CLIs do not implement --version yet; that's tracked as
53
+ Phase 6 work in the implementation plan. This function works against
54
+ today's state."""
55
+ path = _which(binary)
56
+ if path is None:
57
+ return None
58
+
59
+ def _run(args: list[str]) -> str | None:
60
+ try:
61
+ out = subprocess.run(
62
+ [path, *args],
63
+ capture_output=True,
64
+ text=True,
65
+ timeout=5,
66
+ )
67
+ except (subprocess.TimeoutExpired, OSError):
68
+ return None
69
+ return (out.stdout or out.stderr or "").strip() or None
70
+
71
+ text = _run(["--version"])
72
+ if text and not _looks_like_error(text):
73
+ return text.splitlines()[0]
74
+ text = _run(["--help"])
75
+ if text:
76
+ return text.splitlines()[0]
77
+ return None
78
+
79
+
80
+ def _looks_like_error(text: str) -> bool:
81
+ head = text.lower()
82
+ return head.startswith(("error", "usage:", "unknown")) or "unknown command" in head
83
+
84
+
85
+ def cmd_list(argv: list[str]) -> int:
86
+ del argv
87
+ rows = []
88
+ for name in CONSUMERS:
89
+ path = _which(name)
90
+ rows.append({
91
+ "tool": name,
92
+ "installed": path is not None,
93
+ "path": path,
94
+ "version": _version_of(name) if path else None,
95
+ })
96
+ json.dump(rows, sys.stdout, ensure_ascii=False, indent=2)
97
+ sys.stdout.write("\n")
98
+ return 0
99
+
100
+
101
+ def cmd_version(argv: list[str]) -> int:
102
+ del argv
103
+ sys.stdout.write(f"owa {__version__}\n")
104
+ return 0
105
+
106
+
107
+ def cmd_doctor(argv: list[str]) -> int:
108
+ path = _which("owa-doctor")
109
+ if path is None:
110
+ sys.stderr.write("owa-doctor not on PATH\n")
111
+ return 13
112
+ return subprocess.call([path, "probe", *argv])
113
+
114
+
115
+ def cmd_schema(argv: list[str]) -> int:
116
+ """Aggregate `<tool> schema` output across installed consumers.
117
+
118
+ Each consumer either ships a JSON schema (Phase 3+) or doesn't.
119
+ Tools that don't yet support `schema` get a stub entry with
120
+ ``"schema_supported": false``. With ``--tool <name>``, fetches a
121
+ single tool only.
122
+ """
123
+ only = None
124
+ if argv and argv[0] in ("--tool", "-t"):
125
+ if len(argv) < 2:
126
+ sys.stderr.write("--tool requires a value\n")
127
+ return 2
128
+ only = argv[1]
129
+ aggregate = []
130
+ for name in CONSUMERS:
131
+ if only and name != only:
132
+ continue
133
+ path = _which(name)
134
+ if path is None:
135
+ aggregate.append({"tool": name, "installed": False})
136
+ continue
137
+ entry: dict[str, object] = {"tool": name, "installed": True, "path": path}
138
+ try:
139
+ proc = subprocess.run(
140
+ [path, "schema"], capture_output=True, text=True, timeout=5,
141
+ )
142
+ except (subprocess.TimeoutExpired, OSError) as e:
143
+ entry["schema_supported"] = False
144
+ entry["error"] = str(e)
145
+ aggregate.append(entry)
146
+ continue
147
+ if proc.returncode == 0 and proc.stdout.strip():
148
+ try:
149
+ entry["schema"] = json.loads(proc.stdout)
150
+ entry["schema_supported"] = True
151
+ except json.JSONDecodeError:
152
+ entry["schema_supported"] = False
153
+ entry["error"] = "non-JSON output from `schema`"
154
+ else:
155
+ entry["schema_supported"] = False
156
+ aggregate.append(entry)
157
+ json.dump(aggregate, sys.stdout, ensure_ascii=False, indent=2)
158
+ sys.stdout.write("\n")
159
+ return 0
160
+
161
+
162
+ def cmd_help(argv: list[str]) -> int:
163
+ del argv
164
+ sys.stdout.write(__doc__ or "")
165
+ return 0
166
+
167
+
168
+ COMMANDS = {
169
+ "list": cmd_list,
170
+ "version": cmd_version,
171
+ "doctor": cmd_doctor,
172
+ "schema": cmd_schema,
173
+ "help": cmd_help,
174
+ "--help": cmd_help,
175
+ "-h": cmd_help,
176
+ "--version": cmd_version,
177
+ }
178
+
179
+
180
+ def main(argv: list[str] | None = None) -> int:
181
+ argv = list(sys.argv[1:] if argv is None else argv)
182
+ if not argv:
183
+ return cmd_help([])
184
+ cmd = argv[0]
185
+ handler = COMMANDS.get(cmd)
186
+ if handler is None:
187
+ sys.stderr.write(f"unknown command: {cmd}\n")
188
+ return 2
189
+ return handler(argv[1:])
190
+
191
+
192
+ if __name__ == "__main__":
193
+ sys.exit(main())
@@ -0,0 +1,10 @@
1
+ """owa-cal - calendar CLI for Outlook / Microsoft 365."""
2
+
3
+ from owa_core.version import suite_version
4
+
5
+ __version__ = suite_version()
6
+
7
+ # Defined after __version__ so cli.py can safely `from . import __version__`.
8
+ from .cli import main # noqa: E402
9
+
10
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,6 @@
1
+ """`python -m owa_cal` entrypoint."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ sys.exit(main())
@@ -0,0 +1,55 @@
1
+ """Outlook REST HTTP helper.
2
+
3
+ One function: api_request. Returns parsed JSON or None (for
4
+ return-to-caller failures). For auth/permission failures we exit the
5
+ process with a clear message - owa-cal is a CLI, not a library, and
6
+ there is no recovery path for a 401 except telling the user to re-run.
7
+ """
8
+ import urllib.parse
9
+
10
+ from owa_core import http
11
+ from owa_core.errors import (
12
+ AuthExpiredError,
13
+ ConflictError,
14
+ InternalError,
15
+ NetworkError,
16
+ NotFoundError,
17
+ OwaError,
18
+ RateLimitedError,
19
+ ScopeInsufficientError,
20
+ emit_error,
21
+ )
22
+
23
+
24
+ def api_request(method, base, endpoint, access_token, body=None, debug=False):
25
+ """Issue a request against Outlook REST.
26
+
27
+ - `base` and `endpoint` are joined with a single slash.
28
+ - `body` is dict-serialised to JSON when non-None.
29
+ - Returns parsed JSON on 2xx, None on 404/429 (caller decides),
30
+ and exits on 401/403 (unrecoverable without reconfig).
31
+ """
32
+ url = f'{base}/{endpoint}'
33
+ try:
34
+ return http.request(method, url, token=access_token, body=body, debug=debug).json
35
+ except (AuthExpiredError, ScopeInsufficientError) as error:
36
+ raise error
37
+ except (ConflictError, InternalError, NetworkError, NotFoundError, RateLimitedError) as error:
38
+ emit_error(error)
39
+ return None
40
+ except OwaError as error:
41
+ emit_error(error)
42
+ return None
43
+
44
+
45
+ def api_get(base, endpoint, access_token, debug=False):
46
+ return api_request('GET', base, endpoint, access_token, debug=debug)
47
+
48
+
49
+ def build_query(params):
50
+ """Build an OData query string. Values are URL-encoded, keys are
51
+ not (they are $-prefixed OData system params)."""
52
+ parts = []
53
+ for k, v in params.items():
54
+ parts.append(f'{k}={urllib.parse.quote(str(v), safe="")}')
55
+ return '&'.join(parts)
@@ -0,0 +1,40 @@
1
+ """Token acquisition. Audience: outlook (Outlook REST endpoint).
2
+
3
+ owa-cal targets `outlook.office.com`, not Microsoft Graph: the OWA SPA
4
+ client owa-piggy borrows does NOT carry Calendars.ReadWrite on the
5
+ Graph audience - OWA itself calls Outlook REST for calendar.
6
+
7
+ Thin wrapper over owa_core.auth - see owa_drive/auth.py for the
8
+ """
9
+
10
+ from owa_core import auth as _core
11
+ from owa_core.errors import OwaError, emit_error
12
+
13
+ TOOL_NAME = 'owa-cal'
14
+ AUDIENCE = 'outlook'
15
+ API_BASE = 'https://outlook.office.com/api/v2.0'
16
+
17
+ def _log_token_remaining(access, debug):
18
+ _core.log_token_remaining(access, debug)
19
+
20
+
21
+ def _refresh_via_owa_piggy(config, debug=False):
22
+ try:
23
+ token = _core.get_token_for_config(
24
+ config, tool_name=TOOL_NAME, audience=AUDIENCE, debug=debug,
25
+ )
26
+ except OwaError as error:
27
+ emit_error(error)
28
+ return None
29
+ return token.access_token
30
+
31
+
32
+ def do_token_refresh(config, debug=False):
33
+ return _refresh_via_owa_piggy(config, debug=debug)
34
+
35
+
36
+ def setup_auth(config, debug=False):
37
+ token = _core.get_token_for_config(
38
+ config, tool_name=TOOL_NAME, audience=AUDIENCE, debug=debug,
39
+ )
40
+ return token.access_token, API_BASE