wikiops 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.
wikiops-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastian Granda Gallego
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.
wikiops-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: wikiops
3
+ Version: 1.0.0
4
+ Summary: Extensible CLI to automate documentation in wiki platforms (Azure DevOps, Confluence, etc.) using Markdown. Built on a plugin and provider architecture, it enables generating, updating, and structuring documentation as code.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Author: Sebastian Granda Gallego
8
+ Author-email: sgg10.develop@gmail.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
17
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
18
+ Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
19
+ Requires-Dist: typer (>=0.24.1,<0.25.0)
20
+ Requires-Dist: wikiops-sdk (>=1.0.0,<2.0.0)
21
+ Project-URL: Documentation, https://github.com/sgg10/wikiops/tree/main/docs
22
+ Project-URL: Homepage, https://github.com/sgg10/wikiops
23
+ Project-URL: Issues, https://github.com/sgg10/wikiops/issues
24
+ Project-URL: Repository, https://github.com/sgg10/wikiops
25
+ Description-Content-Type: text/markdown
26
+
27
+ # wikiops
28
+
29
+ `wikiops` is the host application and CLI runtime for the WikiOps ecosystem.
30
+
31
+ It orchestrates documentation automation workflows around `wikiops-sdk` by loading configuration, discovering plugins and providers, building execution context, rendering previews, and applying planned changes.
32
+
33
+ ## What This Repository Contains
34
+
35
+ - A command-line interface for WikiOps execution workflows.
36
+ - The host orchestrator that coordinates planning and apply flows.
37
+ - Runtime loading for plugins and providers through Python entry points.
38
+ - YAML configuration loading for providers, profiles, refs, and plugin config.
39
+ - Document loading, diff rendering, and apply delegation.
40
+ - A built-in Azure DevOps Wiki provider.
41
+ - A strict `pytest` suite with coverage enforcement.
42
+
43
+ ## What This Repository Does Not Contain
44
+
45
+ - The public extension contracts and shared domain models.
46
+ - A stable SDK-level API surface for plugins and providers.
47
+ - Built-in documentation plugins.
48
+ - Project-specific documentation business logic.
49
+
50
+ Those concerns belong to `wikiops-sdk` and external plugin repositories.
51
+
52
+ ## Relationship To `wikiops-sdk`
53
+
54
+ The WikiOps ecosystem is intentionally split across repositories:
55
+
56
+ ```text
57
+ plugin -> sdk <- host
58
+ provider -> sdk <- host
59
+ ```
60
+
61
+ - `wikiops-sdk`
62
+ - defines the shared contracts, domain models, and compatibility helpers
63
+ - `wikiops`
64
+ - orchestrates execution around those SDK contracts
65
+ - plugins
66
+ - implement documentation planning logic
67
+ - providers
68
+ - implement persistence and read-side infrastructure
69
+
70
+ Canonical SDK documentation lives in the separate SDK repository:
71
+
72
+ - [`wikiops-sdk README`](https://github.com/sgg10/wikiops-sdk/blob/main/README.md)
73
+ - [`wikiops-sdk architecture`](https://github.com/sgg10/wikiops-sdk/blob/main/docs/architecture.md)
74
+ - [`wikiops-sdk contracts API`](https://github.com/sgg10/wikiops-sdk/blob/main/docs/api/contracts.md)
75
+ - [`wikiops-sdk domain API`](https://github.com/sgg10/wikiops-sdk/blob/main/docs/api/domain.md)
76
+
77
+ ## Quick Start
78
+
79
+ Install the host and its runtime dependencies:
80
+
81
+ ```bash
82
+ poetry install --with test
83
+ ```
84
+
85
+ Inspect the currently available extensions:
86
+
87
+ ```bash
88
+ poetry run wikiops plugins
89
+ poetry run wikiops providers
90
+ ```
91
+
92
+ The host currently ships with a built-in provider implementation:
93
+
94
+ - `azure_devops_wiki`
95
+
96
+ Plugins are expected to be installed separately through Python packages that expose the `wikiops.plugins` entry point group.
97
+
98
+ ### Example Configuration
99
+
100
+ The host reads YAML configuration with provider definitions, profiles, refs, and plugin-specific settings.
101
+
102
+ ```yaml
103
+ providers:
104
+ azdo:
105
+ type: azure_devops_wiki
106
+ organization: acme
107
+ project: engineering
108
+ wiki: platform
109
+
110
+ profiles:
111
+ default:
112
+ provider: azdo
113
+ refs:
114
+ docs_root:
115
+ provider: azdo
116
+ kind: path
117
+ locator:
118
+ path: /Engineering/Teams
119
+ plugins:
120
+ acme.team-docs:
121
+ parent_alias: docs_root
122
+ ```
123
+
124
+ ### Example Input
125
+
126
+ ```yaml
127
+ team_name: Platform
128
+ ```
129
+
130
+ ### Plan A Run
131
+
132
+ Replace `acme.team-docs` with an installed plugin ID shown by `wikiops plugins`.
133
+
134
+ ```bash
135
+ poetry run wikiops run \
136
+ --config wikiops.yaml \
137
+ --profile default \
138
+ --plugin acme.team-docs \
139
+ --input input.yaml
140
+ ```
141
+
142
+ This prints:
143
+
144
+ - the planned `ChangeSet` as JSON
145
+ - a preview diff
146
+
147
+ ### Apply A Run
148
+
149
+ ```bash
150
+ poetry run wikiops run \
151
+ --config wikiops.yaml \
152
+ --profile default \
153
+ --plugin acme.team-docs \
154
+ --input input.yaml \
155
+ --apply
156
+ ```
157
+
158
+ On apply, the CLI also prints the provider `ApplyResult` and exits with code `1` if any operation failed.
159
+
160
+ ## Plugin Resources
161
+
162
+ `wikiops` injects a `PluginResourceProvider` when loading plugin entry points.
163
+
164
+ - Resource paths are relative to the plugin package root.
165
+ - The host does not assume a fixed `resources/` directory.
166
+ - If a plugin stores assets under `resources/`, it should request them as `resources/...`.
167
+ - If a plugin already provides its own `resources` object, the host leaves it untouched.
168
+
169
+ ## Documentation Map
170
+
171
+ Start here depending on your role:
172
+
173
+ - New to the host: [`docs/getting-started.md`](docs/getting-started.md)
174
+ - Need the host architecture: [`docs/architecture.md`](docs/architecture.md)
175
+ - Need the runtime execution lifecycle: [`docs/execution-flow.md`](docs/execution-flow.md)
176
+ - Need the YAML configuration model: [`docs/configuration.md`](docs/configuration.md)
177
+ - Need the host/SDK boundary: [`docs/sdk-relationship.md`](docs/sdk-relationship.md)
178
+ - Using the CLI: [`docs/guides/using-the-cli.md`](docs/guides/using-the-cli.md)
179
+ - Building a plugin for this host: [`docs/guides/build-a-plugin.md`](docs/guides/build-a-plugin.md)
180
+ - Building a provider for this host: [`docs/guides/build-a-provider.md`](docs/guides/build-a-provider.md)
181
+ - Need module-level runtime reference: [`docs/reference/core-modules.md`](docs/reference/core-modules.md)
182
+ - Need the built-in Azure DevOps provider reference: [`docs/reference/azure-devops-wiki.md`](docs/reference/azure-devops-wiki.md)
183
+ - Need host internal boundaries: [`docs/reference/internal-boundaries.md`](docs/reference/internal-boundaries.md)
184
+
185
+ The full host documentation index lives at [`docs/index.md`](docs/index.md).
186
+
187
+ ## Tests
188
+
189
+ This repository ships with a strict `pytest` suite and coverage threshold.
190
+
191
+ ```bash
192
+ poetry run pytest
193
+ ```
194
+
195
+ The tests are also useful as executable examples of the current host behavior.
196
+
@@ -0,0 +1,169 @@
1
+ # wikiops
2
+
3
+ `wikiops` is the host application and CLI runtime for the WikiOps ecosystem.
4
+
5
+ It orchestrates documentation automation workflows around `wikiops-sdk` by loading configuration, discovering plugins and providers, building execution context, rendering previews, and applying planned changes.
6
+
7
+ ## What This Repository Contains
8
+
9
+ - A command-line interface for WikiOps execution workflows.
10
+ - The host orchestrator that coordinates planning and apply flows.
11
+ - Runtime loading for plugins and providers through Python entry points.
12
+ - YAML configuration loading for providers, profiles, refs, and plugin config.
13
+ - Document loading, diff rendering, and apply delegation.
14
+ - A built-in Azure DevOps Wiki provider.
15
+ - A strict `pytest` suite with coverage enforcement.
16
+
17
+ ## What This Repository Does Not Contain
18
+
19
+ - The public extension contracts and shared domain models.
20
+ - A stable SDK-level API surface for plugins and providers.
21
+ - Built-in documentation plugins.
22
+ - Project-specific documentation business logic.
23
+
24
+ Those concerns belong to `wikiops-sdk` and external plugin repositories.
25
+
26
+ ## Relationship To `wikiops-sdk`
27
+
28
+ The WikiOps ecosystem is intentionally split across repositories:
29
+
30
+ ```text
31
+ plugin -> sdk <- host
32
+ provider -> sdk <- host
33
+ ```
34
+
35
+ - `wikiops-sdk`
36
+ - defines the shared contracts, domain models, and compatibility helpers
37
+ - `wikiops`
38
+ - orchestrates execution around those SDK contracts
39
+ - plugins
40
+ - implement documentation planning logic
41
+ - providers
42
+ - implement persistence and read-side infrastructure
43
+
44
+ Canonical SDK documentation lives in the separate SDK repository:
45
+
46
+ - [`wikiops-sdk README`](https://github.com/sgg10/wikiops-sdk/blob/main/README.md)
47
+ - [`wikiops-sdk architecture`](https://github.com/sgg10/wikiops-sdk/blob/main/docs/architecture.md)
48
+ - [`wikiops-sdk contracts API`](https://github.com/sgg10/wikiops-sdk/blob/main/docs/api/contracts.md)
49
+ - [`wikiops-sdk domain API`](https://github.com/sgg10/wikiops-sdk/blob/main/docs/api/domain.md)
50
+
51
+ ## Quick Start
52
+
53
+ Install the host and its runtime dependencies:
54
+
55
+ ```bash
56
+ poetry install --with test
57
+ ```
58
+
59
+ Inspect the currently available extensions:
60
+
61
+ ```bash
62
+ poetry run wikiops plugins
63
+ poetry run wikiops providers
64
+ ```
65
+
66
+ The host currently ships with a built-in provider implementation:
67
+
68
+ - `azure_devops_wiki`
69
+
70
+ Plugins are expected to be installed separately through Python packages that expose the `wikiops.plugins` entry point group.
71
+
72
+ ### Example Configuration
73
+
74
+ The host reads YAML configuration with provider definitions, profiles, refs, and plugin-specific settings.
75
+
76
+ ```yaml
77
+ providers:
78
+ azdo:
79
+ type: azure_devops_wiki
80
+ organization: acme
81
+ project: engineering
82
+ wiki: platform
83
+
84
+ profiles:
85
+ default:
86
+ provider: azdo
87
+ refs:
88
+ docs_root:
89
+ provider: azdo
90
+ kind: path
91
+ locator:
92
+ path: /Engineering/Teams
93
+ plugins:
94
+ acme.team-docs:
95
+ parent_alias: docs_root
96
+ ```
97
+
98
+ ### Example Input
99
+
100
+ ```yaml
101
+ team_name: Platform
102
+ ```
103
+
104
+ ### Plan A Run
105
+
106
+ Replace `acme.team-docs` with an installed plugin ID shown by `wikiops plugins`.
107
+
108
+ ```bash
109
+ poetry run wikiops run \
110
+ --config wikiops.yaml \
111
+ --profile default \
112
+ --plugin acme.team-docs \
113
+ --input input.yaml
114
+ ```
115
+
116
+ This prints:
117
+
118
+ - the planned `ChangeSet` as JSON
119
+ - a preview diff
120
+
121
+ ### Apply A Run
122
+
123
+ ```bash
124
+ poetry run wikiops run \
125
+ --config wikiops.yaml \
126
+ --profile default \
127
+ --plugin acme.team-docs \
128
+ --input input.yaml \
129
+ --apply
130
+ ```
131
+
132
+ On apply, the CLI also prints the provider `ApplyResult` and exits with code `1` if any operation failed.
133
+
134
+ ## Plugin Resources
135
+
136
+ `wikiops` injects a `PluginResourceProvider` when loading plugin entry points.
137
+
138
+ - Resource paths are relative to the plugin package root.
139
+ - The host does not assume a fixed `resources/` directory.
140
+ - If a plugin stores assets under `resources/`, it should request them as `resources/...`.
141
+ - If a plugin already provides its own `resources` object, the host leaves it untouched.
142
+
143
+ ## Documentation Map
144
+
145
+ Start here depending on your role:
146
+
147
+ - New to the host: [`docs/getting-started.md`](docs/getting-started.md)
148
+ - Need the host architecture: [`docs/architecture.md`](docs/architecture.md)
149
+ - Need the runtime execution lifecycle: [`docs/execution-flow.md`](docs/execution-flow.md)
150
+ - Need the YAML configuration model: [`docs/configuration.md`](docs/configuration.md)
151
+ - Need the host/SDK boundary: [`docs/sdk-relationship.md`](docs/sdk-relationship.md)
152
+ - Using the CLI: [`docs/guides/using-the-cli.md`](docs/guides/using-the-cli.md)
153
+ - Building a plugin for this host: [`docs/guides/build-a-plugin.md`](docs/guides/build-a-plugin.md)
154
+ - Building a provider for this host: [`docs/guides/build-a-provider.md`](docs/guides/build-a-provider.md)
155
+ - Need module-level runtime reference: [`docs/reference/core-modules.md`](docs/reference/core-modules.md)
156
+ - Need the built-in Azure DevOps provider reference: [`docs/reference/azure-devops-wiki.md`](docs/reference/azure-devops-wiki.md)
157
+ - Need host internal boundaries: [`docs/reference/internal-boundaries.md`](docs/reference/internal-boundaries.md)
158
+
159
+ The full host documentation index lives at [`docs/index.md`](docs/index.md).
160
+
161
+ ## Tests
162
+
163
+ This repository ships with a strict `pytest` suite and coverage threshold.
164
+
165
+ ```bash
166
+ poetry run pytest
167
+ ```
168
+
169
+ The tests are also useful as executable examples of the current host behavior.
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [project]
6
+ name = "wikiops"
7
+ version = "1.0.0"
8
+ description = "Extensible CLI to automate documentation in wiki platforms (Azure DevOps, Confluence, etc.) using Markdown. Built on a plugin and provider architecture, it enables generating, updating, and structuring documentation as code."
9
+ authors = [
10
+ {name = "Sebastian Granda Gallego",email = "sgg10.develop@gmail.com"}
11
+ ]
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ readme = "README.md"
15
+ requires-python = ">=3.10,<4.0"
16
+ dependencies = [
17
+ "pydantic (>=2.12.5,<3.0.0)",
18
+ "typer (>=0.24.1,<0.25.0)",
19
+ "pyyaml (>=6.0.3,<7.0.0)",
20
+ "httpx (>=0.28.1,<0.29.0)",
21
+ "wikiops-sdk (>=1.0.0,<2.0.0)"
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/sgg10/wikiops"
26
+ Repository = "https://github.com/sgg10/wikiops"
27
+ Documentation = "https://github.com/sgg10/wikiops/tree/main/docs"
28
+ Issues = "https://github.com/sgg10/wikiops/issues"
29
+
30
+ [project.scripts]
31
+ wikiops = "wikiops.cli.app:app"
32
+
33
+ [project.entry-points."wikiops.providers"]
34
+ azure_devops_wiki = "wikiops.providers.azure_devops:AzureDevOpsWikiProviderFactory"
35
+
36
+ [tool.poetry]
37
+ packages = [
38
+ {include = "wikiops", from = "src"}
39
+ ]
40
+
41
+ [dependency-groups]
42
+ test = [
43
+ "pytest (>=9.0.3,<10.0.0)",
44
+ "pytest-cov (>=6.0.0,<7.0.0)"
45
+ ]
46
+
47
+ [tool.pytest.ini_options]
48
+ addopts = "--strict-config --strict-markers -ra --cov=wikiops --cov-branch --cov-report=term-missing --cov-report=xml --cov-fail-under=90"
49
+ testpaths = ["tests"]
50
+ pythonpath = ["src"]
51
+ xfail_strict = true
52
+ required_plugins = ["pytest-cov"]
53
+
54
+ [tool.coverage.run]
55
+ branch = true
56
+ source = ["wikiops"]
57
+
58
+ [tool.coverage.report]
59
+ show_missing = true
60
+ skip_covered = false
@@ -0,0 +1,8 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("wikiops")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0+local"
7
+
8
+ all = ["__version__"]
File without changes
@@ -0,0 +1,81 @@
1
+ from typing import Any
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+ import typer
6
+
7
+ from wikiops.core.orchestrator import DefaultDocumentationOrchestrator
8
+
9
+ app = typer.Typer(
10
+ help="WikiOps CLI - A tool for Markdown-based documentation automation."
11
+ )
12
+
13
+
14
+ def _load_yaml(path: str | Path) -> dict[str, Any]:
15
+ return yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
16
+
17
+
18
+ @app.command("plugins")
19
+ def list_plugins() -> None:
20
+ """List discovered plugins."""
21
+
22
+ orchestrator = DefaultDocumentationOrchestrator()
23
+ for plugin in orchestrator.plugin_manager.list():
24
+ typer.echo(
25
+ f"- {plugin.manifest.plugin_id} :: {plugin.manifest.display_name} ({plugin.manifest.version})"
26
+ )
27
+
28
+
29
+ @app.command("providers")
30
+ def list_providers() -> None:
31
+ """List discovered provider types."""
32
+
33
+ orchestrator = DefaultDocumentationOrchestrator()
34
+ for provider_type in orchestrator.provider_manager.list_types():
35
+ typer.echo(f"- {provider_type}")
36
+
37
+
38
+ @app.command("run")
39
+ def run(
40
+ config: str = typer.Option(
41
+ ..., "--config", "-c", help="Path to the YAML config file."
42
+ ),
43
+ profile: str = typer.Option(
44
+ ..., "--profile", "-p", help="Profile name to execute."
45
+ ),
46
+ plugin: str = typer.Option(
47
+ ..., "--plugin", help="Plugin identifier or entry point name."
48
+ ),
49
+ input: str = typer.Option(
50
+ ..., "--input", "-i", help="Path to the YAML input file."
51
+ ),
52
+ apply: bool = typer.Option(False, "--apply", help="Persist the planned changes."),
53
+ ) -> None:
54
+ """Plan or apply a documentation use case."""
55
+
56
+ orchestrator = DefaultDocumentationOrchestrator()
57
+ raw_input = _load_yaml(input)
58
+
59
+ if apply:
60
+ change_set, apply_result, diff = orchestrator.apply_from_file(
61
+ config, profile, plugin, raw_input
62
+ )
63
+ typer.echo("=== CHANGESET ===")
64
+ typer.echo(change_set.model_dump_json(indent=2))
65
+ typer.echo("\n=== DIFF ===")
66
+ typer.echo(diff or "(No diff available)")
67
+ typer.echo("\n=== APPLY RESULT ===")
68
+ typer.echo(apply_result.model_dump_json(indent=2))
69
+ raise typer.Exit(code=1 if apply_result.has_failures() else 0)
70
+
71
+ _, _, change_set, diff = orchestrator.plan_from_file(
72
+ config, profile, plugin, raw_input, dry_run=True
73
+ )
74
+ typer.echo("=== CHANGESET ===")
75
+ typer.echo(change_set.model_dump_json(indent=2))
76
+ typer.echo("\n=== DIFF ===")
77
+ typer.echo(diff or "(No diff generated)")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ app()
File without changes
@@ -0,0 +1,10 @@
1
+ from wikiops_sdk.contracts import DocumentProvider
2
+ from wikiops_sdk.domain import ApplyResult, ChangeSet
3
+
4
+
5
+ class ApplyEngine:
6
+ """Persists a planned change set through a provider."""
7
+
8
+ def apply(self, provider: DocumentProvider, change_set: ChangeSet) -> ApplyResult:
9
+ """Applies the given change set using the provided document provider."""
10
+ return provider.apply_changes(change_set)
@@ -0,0 +1,98 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict, Union
3
+
4
+ import yaml
5
+ from pydantic import BaseModel, Field
6
+
7
+ from wikiops.core.exceptions import ConfigurationError
8
+ from wikiops_sdk.domain import DocumentRef
9
+
10
+
11
+ class ProviderDefinition(BaseModel):
12
+ """Provider configuration entry from the YAML file."""
13
+
14
+ type: str
15
+ settings: Dict[str, Any] = Field(default_factory=dict)
16
+
17
+
18
+ class ProfileDefinition(BaseModel):
19
+ """Execution profile entry from the YAML file."""
20
+
21
+ provider: str
22
+ refs: Dict[str, DocumentRef] = Field(default_factory=dict)
23
+ plugins: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
24
+
25
+
26
+ class AppConfig(BaseModel):
27
+ """Top-level application configuration."""
28
+
29
+ providers: Dict[str, ProviderDefinition] = Field(default_factory=dict)
30
+ profiles: Dict[str, ProfileDefinition] = Field(default_factory=dict)
31
+
32
+
33
+ class ConfigLoader:
34
+ """Loads and validates YAML application configuration."""
35
+
36
+ def _prepare_providers(
37
+ self, raw_providers: Dict[str, Any]
38
+ ) -> Dict[str, ProviderDefinition]:
39
+ """Validates and transforms raw provider definitions into structured ProviderDefinition instances."""
40
+ providers = {}
41
+
42
+ for name, provider in raw_providers.items():
43
+ provider_type = provider.get("type")
44
+
45
+ if not provider_type:
46
+ raise ConfigurationError(f"Provider '{name}' is missing 'type' field.")
47
+
48
+ settings = {k: v for k, v in provider.items() if k != "type"}
49
+ settings.setdefault("provider_name", name)
50
+
51
+ providers[name] = ProviderDefinition(type=provider_type, settings=settings)
52
+
53
+ return providers
54
+
55
+ def _prepare_profiles(
56
+ self, raw_profiles: Dict[str, Any]
57
+ ) -> Dict[str, ProfileDefinition]:
58
+ """Validates and transforms raw profile definitions into structured ProfileDefinition instances."""
59
+ profiles = {}
60
+
61
+ for name, profile in raw_profiles.items():
62
+ provider_name = profile.get("provider")
63
+
64
+ if not provider_name:
65
+ raise ConfigurationError(
66
+ f"Profile '{name}' is missing 'provider' field."
67
+ )
68
+
69
+ refs_raw = profile.get("refs", {})
70
+ plugins_raw = profile.get("plugins", {})
71
+
72
+ refs = {
73
+ alias: DocumentRef.model_validate(ref_raw)
74
+ for alias, ref_raw in refs_raw.items()
75
+ }
76
+ profiles[name] = ProfileDefinition(
77
+ provider=provider_name, refs=refs, plugins=plugins_raw
78
+ )
79
+
80
+ return profiles
81
+
82
+ def load(self, path: Union[str, Path]) -> AppConfig:
83
+ config_path = Path(path)
84
+
85
+ if not config_path.exists():
86
+ raise ConfigurationError(f"Configuration file not found: {config_path}")
87
+
88
+ if not config_path.is_file():
89
+ raise ConfigurationError(f"Configuration path is not a file: {config_path}")
90
+
91
+ raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
92
+ providers_section = raw.get("providers", {})
93
+ profiles_section = raw.get("profiles", {})
94
+
95
+ providers = self._prepare_providers(providers_section)
96
+ profiles = self._prepare_profiles(profiles_section)
97
+
98
+ return AppConfig(providers=providers, profiles=profiles)
@@ -0,0 +1,42 @@
1
+ from typing import Dict, List
2
+ from difflib import unified_diff
3
+
4
+ from wikiops_sdk.domain import ChangeSet, UpdateDocumentOperation
5
+
6
+
7
+ class DiffEngine:
8
+ """Builds preview diffs for update operations."""
9
+
10
+ @staticmethod
11
+ def _find_document(documents: Dict[str, object], ref: object):
12
+ for document in documents.values():
13
+ if getattr(document, "ref", None) == ref:
14
+ return document
15
+ return None
16
+
17
+ def render(self, documents: Dict[str, object], change_set: ChangeSet) -> str:
18
+ chunks: List[str] = []
19
+ for op in change_set.operations:
20
+ if not isinstance(op, UpdateDocumentOperation):
21
+ chunks.append(
22
+ f"# Operation {op.operation_id}\n"
23
+ f"Type: {op.operation}\n"
24
+ f"This operation creates a new content and has no line diff against an existing document."
25
+ )
26
+ continue
27
+
28
+ current_doc = self._find_document(documents, op.ref)
29
+ current_lines = (current_doc.content if current_doc else "").splitlines()
30
+ new_lines = op.new_content.splitlines()
31
+
32
+ diff = unified_diff(
33
+ current_lines,
34
+ new_lines,
35
+ fromfile="current",
36
+ tofile="planned",
37
+ lineterm="",
38
+ )
39
+
40
+ chunks.append(f"# Operation {op.operation_id}\n" + "\n".join(diff))
41
+
42
+ return "\n\n".join(chunks)