Metadata-Version: 2.4
Name: scalio-m365
Version: 0.1.0
Summary: Scalio: write Microsoft 365 settings (Entra, SharePoint, Intune, Exchange) to a live tenant, shared by Foundry and Citadel
License: Proprietary
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"

# scalio-m365

Shared code that writes Microsoft 365 settings to a live tenant («Scalio Schreiben»): one writer per
Microsoft365DSC resource type (Entra ID, SharePoint, Intune, Exchange Online / Defender for Office 365), with the
property names of the governance catalog (scalio-catalog). Foundry rolls baselines out with it, Citadel heals
deviations. Decision: scalio-docs `concepts/m365-schreibpaket.md` (`m365pkg/P1`–`P4`), ADR 0089.

Nothing in this package is secret and it holds no content: the baselines stay in the private repo
`scalio_blueprint`, tokens and certificates stay in the app (scalio-tenant).

## Use in an app

```python
import scalio_m365 as m365

g = m365.Graph(call, exo, create=True)        # Foundry; Citadel: create=False (keeps existing objects only)
reason = m365.why_not(type_, desired)          # Citadel: why_not(type_, desired, change_only=True)
actual = await m365.read_one(g, type_, match)  # catalog names
after = m365.fields(type_, {**(actual or {}), **desired})
if not m365.same(actual, after):
    await m365.kind(type_).write(g, actual, after)
```

- `call(method, path, body) -> dict`: one Microsoft Graph call (`path` relative to `/v1.0`, or a full
  `@odata.nextLink`); raise on an error. `exo(cmdlet, parameters) -> list[dict]`: one cmdlet through the Exchange
  admin API (`InvokeCommand`, app-only token). The app brings token, HTTP client and logging.
- `WriteError`: the step cannot be written as asked (missing group, empty break-glass group, object missing,
  no reviewer, no notification address); the text says what to do.
- `SCOPES` (Graph permissions), `exchange.SCOPES`, `DIRECTORY_ROLES`, `READ_ALONG` and `needs(type_)`: what the
  «Scalio Schreiben» registration needs.
- Exchange in a fresh tenant: `exchange.prepare(g, is_dehydrated=..., customised_at=...)` runs
  `Enable-OrganizationCustomization` once and says when to wait; the app stores the time.
- `scalio_m365.mapping`: pure name mappings (Graph answer -> catalog names) for the apps' readers.
- `scalio_m365.testing.FakeMicrosoft`: fake Graph and Exchange for the apps' tests.

## Modes

| | Foundry (`create=True`) | Citadel (`create=False`, `change_only=True`) |
| --- | --- | --- |
| Entra, SharePoint | creates or changes | creates or changes |
| Intune policy missing | creates (compliance with a block action) | refuses: create it with its assignments first |
| Exchange policy missing | Defender policies with a rule per accepted domain, DKIM | refuses |
| Names of Intune/Exchange objects | set on create | never changed (`why_not` says so) |

## Development

```bash
pip install -e ".[dev]"
pytest && ruff check .
```

Release: bump `pyproject.toml` and `__version__`, reserve the version in scalio-docs `REGISTER.md`, run
`tools/changelog.py --repo scalio-m365 release <version>` there, then «Freigeben» in the cockpit (workflow
`publish.yml`, PyPI trusted publishing, environment `pypi`). Tags without `v`.
