phlo-clickstack 0.3.1__tar.gz → 0.4.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 (25) hide show
  1. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/PKG-INFO +2 -2
  2. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/pyproject.toml +2 -2
  3. phlo_clickstack-0.4.0/src/phlo_clickstack/authorization.py +27 -0
  4. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack/cli.py +8 -6
  5. phlo_clickstack-0.4.0/src/phlo_clickstack/cli_plugin.py +17 -0
  6. phlo_clickstack-0.4.0/src/phlo_clickstack/plugin.py +15 -0
  7. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/PKG-INFO +2 -2
  8. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/requires.txt +1 -1
  9. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/tests/test_clickstack_cli.py +30 -0
  10. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/tests/test_clickstack_plugin.py +17 -0
  11. phlo_clickstack-0.3.1/src/phlo_clickstack/authorization.py +0 -166
  12. phlo_clickstack-0.3.1/src/phlo_clickstack/cli_plugin.py +0 -49
  13. phlo_clickstack-0.3.1/src/phlo_clickstack/plugin.py +0 -44
  14. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/README.md +0 -0
  15. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/setup.cfg +0 -0
  16. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack/__init__.py +0 -0
  17. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack/observability_backend.py +0 -0
  18. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack/resource_provider.py +0 -0
  19. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack/service.yaml +0 -0
  20. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/SOURCES.txt +0 -0
  21. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/dependency_links.txt +0 -0
  22. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/entry_points.txt +0 -0
  23. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/top_level.txt +0 -0
  24. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/tests/test_authorization.py +0 -0
  25. {phlo_clickstack-0.3.1 → phlo_clickstack-0.4.0}/tests/test_observability_backend.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phlo-clickstack
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: clickstack service plugin for Phlo
5
5
  Author-email: Phlo Team <team@phlo.dev>
6
6
  License: MIT
7
7
  Requires-Python: >=3.11
8
8
  Description-Content-Type: text/plain
9
9
  Requires-Dist: phlo>=0.1.0
10
- Requires-Dist: pyyaml>=6.0.1
10
+ Requires-Dist: requests>=2.32.5
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=7.0; extra == "dev"
13
13
  Requires-Dist: ruff>=0.1.0; extra == "dev"
@@ -8,12 +8,12 @@ requires = [
8
8
  [project]
9
9
  dependencies = [
10
10
  "phlo>=0.1.0",
11
- "pyyaml>=6.0.1",
11
+ "requests>=2.32.5",
12
12
  ]
13
13
  description = "clickstack service plugin for Phlo"
14
14
  name = "phlo-clickstack"
15
15
  requires-python = ">=3.11"
16
- version = "0.3.1"
16
+ version = "0.4.0"
17
17
 
18
18
  [[project.authors]]
19
19
  email = "team@phlo.dev"
@@ -0,0 +1,27 @@
1
+ """phlo_clickstack CLI authorization table."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from phlo.cli.authorization import cli_surface_adapter_class
6
+
7
+ SURFACE_NAME = "phlo-clickstack"
8
+ FRAMEWORK_TYPE = "cli"
9
+ MUTATION_COMMANDS: frozenset[str] = frozenset(["clickstack.query"])
10
+ READ_COMMANDS: frozenset[str] = frozenset([])
11
+ COMMAND_RESOURCE_MAP: dict[str, str] = {"clickstack.query": "dataset"}
12
+ COMMAND_ACTION_MAP: dict[str, str] = {"clickstack.query": "dataset.write"}
13
+
14
+ ClickStackSurfaceAdapter = cli_surface_adapter_class(
15
+ "ClickStackSurfaceAdapter",
16
+ surface_name=SURFACE_NAME,
17
+ mutation_commands=MUTATION_COMMANDS,
18
+ read_commands=READ_COMMANDS,
19
+ command_resource_map=COMMAND_RESOURCE_MAP,
20
+ command_action_map=COMMAND_ACTION_MAP,
21
+ default_action_prefix="clickstack",
22
+ default_resource_prefix="clickstack",
23
+ )
24
+
25
+
26
+ def get_adapter() -> ClickStackSurfaceAdapter:
27
+ return ClickStackSurfaceAdapter.get_instance()
@@ -13,7 +13,9 @@ from subprocess import TimeoutExpired
13
13
 
14
14
  import click
15
15
 
16
+ from phlo.cli.authorization_wrappers import enforce_surface_mutation_authorization
16
17
  from phlo.cli.commands.services import utils as services_utils
18
+ from phlo.cli.commands.services.utils import ensure_compose_project
17
19
  from phlo.cli.infrastructure.command import CommandError, run_command
18
20
  from phlo.cli.infrastructure.compose import compose_base_cmd
19
21
  from phlo.cli.infrastructure.utils import get_project_name
@@ -21,10 +23,11 @@ from phlo.cli.output import (
21
23
  empty_file_error,
22
24
  exclusive_options_error,
23
25
  file_read_error,
24
- missing_phlo_project_error,
25
26
  missing_query_error,
26
27
  )
28
+ from phlo.cli.sql import is_mutating_sql
27
29
  from phlo.logging import get_logger
30
+ from phlo_clickstack.authorization import get_adapter as get_clickstack_adapter
28
31
 
29
32
  logger = get_logger(__name__)
30
33
 
@@ -78,10 +81,7 @@ def _ensure_phlo_dir() -> Path:
78
81
  click.ClickException: If .phlo directory does not exist.
79
82
 
80
83
  """
81
- phlo_dir = Path.cwd() / ".phlo"
82
- if phlo_dir.exists():
83
- return phlo_dir
84
- raise missing_phlo_project_error()
84
+ return ensure_compose_project()
85
85
 
86
86
 
87
87
  def _require_container_backend() -> None:
@@ -130,10 +130,12 @@ def clickstack_query(
130
130
  click.ClickException: If query times out.
131
131
 
132
132
  """
133
+ sql = _read_query(query=query, file=query_file)
134
+ if is_mutating_sql(sql):
135
+ enforce_surface_mutation_authorization("clickstack.query", get_clickstack_adapter)
133
136
  _require_container_backend()
134
137
  phlo_dir = _ensure_phlo_dir()
135
138
  project_name = get_project_name()
136
- sql = _read_query(query=query, file=query_file)
137
139
 
138
140
  cmd = compose_base_cmd(phlo_dir=phlo_dir, project_name=project_name)
139
141
  cmd.extend(
@@ -0,0 +1,17 @@
1
+ """Clickstack CLI plugin registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from phlo.plugins.base import cli_command_plugin_class
7
+
8
+ from phlo_clickstack.cli import clickstack_group
9
+
10
+
11
+ ClickStackCliPlugin = cli_command_plugin_class(
12
+ "ClickStackCliPlugin",
13
+ name="clickstack",
14
+ version="0.1.0",
15
+ description="CLI commands for ClickStack query access",
16
+ commands=[clickstack_group],
17
+ )
@@ -0,0 +1,15 @@
1
+ """Clickstack service plugin registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from phlo.plugins import service_plugin_class
6
+
7
+
8
+ ClickStackServicePlugin = service_plugin_class(
9
+ "ClickStackServicePlugin",
10
+ name="clickstack",
11
+ version="0.1.0",
12
+ description="ClickStack all-in-one observability backend",
13
+ author="Phlo Team",
14
+ tags=["observability", "logs", "metrics", "traces"],
15
+ )
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phlo-clickstack
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: clickstack service plugin for Phlo
5
5
  Author-email: Phlo Team <team@phlo.dev>
6
6
  License: MIT
7
7
  Requires-Python: >=3.11
8
8
  Description-Content-Type: text/plain
9
9
  Requires-Dist: phlo>=0.1.0
10
- Requires-Dist: pyyaml>=6.0.1
10
+ Requires-Dist: requests>=2.32.5
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=7.0; extra == "dev"
13
13
  Requires-Dist: ruff>=0.1.0; extra == "dev"
@@ -1,5 +1,5 @@
1
1
  phlo>=0.1.0
2
- pyyaml>=6.0.1
2
+ requests>=2.32.5
3
3
 
4
4
  [dev]
5
5
  pytest>=7.0
@@ -51,6 +51,36 @@ def test_clickstack_query_runs_clickhouse_client(monkeypatch) -> None:
51
51
  assert result.output == '{"1":1}\n'
52
52
 
53
53
 
54
+ def test_clickstack_query_authorizes_only_mutating_sql(monkeypatch) -> None:
55
+ calls: list[str] = []
56
+
57
+ monkeypatch.setattr("phlo_clickstack.cli._ensure_phlo_dir", lambda: Path("/tmp/project/.phlo"))
58
+ monkeypatch.setattr(
59
+ "phlo_clickstack.cli._require_container_backend",
60
+ lambda: calls.append("backend"),
61
+ )
62
+ monkeypatch.setattr("phlo_clickstack.cli.get_project_name", lambda: "demo")
63
+ monkeypatch.setattr(
64
+ "phlo_clickstack.cli.compose_base_cmd",
65
+ lambda **_kwargs: ["docker", "compose", "-p", "demo"],
66
+ )
67
+ monkeypatch.setattr(
68
+ "phlo_clickstack.cli.run_command",
69
+ lambda cmd, **_kwargs: CompletedProcess(cmd, 0, stdout="ok\n", stderr=""),
70
+ )
71
+ monkeypatch.setattr(
72
+ "phlo_clickstack.cli.enforce_surface_mutation_authorization",
73
+ lambda *_args, **_kwargs: calls.append("auth"),
74
+ )
75
+
76
+ select_result = CliRunner().invoke(clickstack_group, ["query", "SELECT 1"])
77
+ insert_result = CliRunner().invoke(clickstack_group, ["query", "INSERT INTO t VALUES (1)"])
78
+
79
+ assert select_result.exit_code == 0
80
+ assert insert_result.exit_code == 0
81
+ assert calls == ["backend", "auth", "backend"]
82
+
83
+
54
84
  def test_clickstack_query_uses_selected_container_backend(monkeypatch, tmp_path) -> None:
55
85
  phlo_dir = tmp_path / ".phlo"
56
86
  phlo_dir.mkdir()
@@ -1,5 +1,8 @@
1
1
  """Tests for ClickStack service plugin."""
2
2
 
3
+ from click.testing import CliRunner
4
+
5
+ from phlo_clickstack.cli import clickstack_group
3
6
  from phlo_clickstack.plugin import ClickStackServicePlugin
4
7
 
5
8
 
@@ -25,3 +28,17 @@ def test_clickstack_plugin_metadata() -> None:
25
28
 
26
29
  assert meta.name == "clickstack"
27
30
  assert "observability" in meta.tags
31
+
32
+
33
+ def test_clickstack_query_rejects_partial_phlo_directory(monkeypatch, tmp_path) -> None:
34
+ """Logging-created .phlo directories are not initialized service projects."""
35
+ monkeypatch.chdir(tmp_path)
36
+ (tmp_path / ".phlo" / "logs").mkdir(parents=True)
37
+ monkeypatch.setattr("phlo_clickstack.cli._require_container_backend", lambda: None)
38
+
39
+ result = CliRunner().invoke(clickstack_group, ["query", "SELECT 1"])
40
+
41
+ assert result.exit_code != 0
42
+ assert "Phlo services have not been initialized" in result.output
43
+ assert "Missing: .phlo/docker-compose.yml" in result.output
44
+ assert "Run: phlo services init" in result.output
@@ -1,166 +0,0 @@
1
- """Regulated surface adapter for ClickStack CLI.
2
-
3
- This module provides the regulated surface adapter for the ClickStack CLI,
4
- declaring mutation commands that require authorization and enforcing
5
- through core enforcement.
6
-
7
- Mutation commands (require authorization):
8
- - clickstack.query
9
-
10
- Read commands (no authorization):
11
- - (none currently)
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import os
17
- import threading
18
- from typing import Any
19
-
20
- from phlo.cli.authorization import CliPrincipalResolver
21
- from phlo.logging import get_logger
22
- from phlo.security.adapters import (
23
- EnforcementResult,
24
- SurfaceOperation,
25
- )
26
- from phlo.security.enforcement import enforce
27
-
28
- logger = get_logger(__name__)
29
-
30
- SURFACE_NAME = "phlo-clickstack"
31
- FRAMEWORK_TYPE = "cli"
32
-
33
- MUTATION_COMMANDS: frozenset[str] = frozenset(
34
- {
35
- "clickstack.query",
36
- }
37
- )
38
-
39
- READ_COMMANDS: frozenset[str] = frozenset()
40
-
41
- COMMAND_RESOURCE_MAP: dict[str, str] = {
42
- "clickstack.query": "dataset",
43
- }
44
-
45
- COMMAND_ACTION_MAP: dict[str, str] = {
46
- "clickstack.query": "dataset.write",
47
- }
48
-
49
-
50
- class ClickStackSurfaceAdapter:
51
- """Regulated surface adapter for ClickStack CLI.
52
-
53
- Declares mutation commands and enforces authorization through core
54
- enforcement for privileged operations.
55
- """
56
-
57
- _instance: ClickStackSurfaceAdapter | None = None
58
- _lock = threading.Lock()
59
-
60
- def __init__(self) -> None:
61
- self._resolver = CliPrincipalResolver()
62
-
63
- @classmethod
64
- def get_instance(cls) -> ClickStackSurfaceAdapter:
65
- if cls._instance is None:
66
- with cls._lock:
67
- if cls._instance is None:
68
- cls._instance = cls()
69
- return cls._instance
70
-
71
- @property
72
- def surface_name(self) -> str:
73
- return SURFACE_NAME
74
-
75
- @property
76
- def framework_type(self) -> str:
77
- return FRAMEWORK_TYPE
78
-
79
- def list_operations(self) -> list[SurfaceOperation]:
80
- operations: list[SurfaceOperation] = []
81
- for command in MUTATION_COMMANDS:
82
- resource_type = COMMAND_RESOURCE_MAP.get(command, "dataset")
83
- action = COMMAND_ACTION_MAP.get(command, f"clickstack.{command}")
84
- operations.append(
85
- SurfaceOperation(
86
- action=action,
87
- resource_type=resource_type,
88
- operation_name=command,
89
- resource_id_strategy=None,
90
- framework_metadata={"command": command},
91
- )
92
- )
93
- return operations
94
-
95
- def is_active(self, runtime: Any) -> bool:
96
- return True
97
-
98
- def install(self, runtime: Any) -> None:
99
- pass
100
-
101
- def enforce_mutation(self, command: str, resource_id: str | None = None) -> EnforcementResult:
102
- """Enforce authorization for a mutation command."""
103
- if command not in MUTATION_COMMANDS:
104
- return EnforcementResult.allow()
105
-
106
- action = COMMAND_ACTION_MAP.get(command, f"clickstack.{command}")
107
- resource_type = COMMAND_RESOURCE_MAP.get(command, "dataset")
108
- resource_id_final = resource_id or f"clickstack:{command}"
109
-
110
- principal = self._resolver.resolve()
111
-
112
- from phlo.capabilities.interfaces import ResourceRef
113
-
114
- resource = ResourceRef(
115
- resource_type=resource_type,
116
- resource_id=resource_id_final,
117
- )
118
-
119
- request_id = os.environ.get("PHLO_REQUEST_ID")
120
-
121
- result = enforce(
122
- principal=principal,
123
- action=action,
124
- resource=resource,
125
- context=None,
126
- request_id=request_id,
127
- surface=SURFACE_NAME,
128
- )
129
-
130
- logger.debug(
131
- "clickstack_mutation_enforcement_result",
132
- command=command,
133
- action=action,
134
- result=result.variant,
135
- subject=principal.subject,
136
- )
137
-
138
- return result
139
-
140
- def check_command_authorization(self, command_path: str) -> EnforcementResult:
141
- """Check if a command is authorized to run."""
142
- if command_path in READ_COMMANDS:
143
- return EnforcementResult.allow()
144
-
145
- if command_path in MUTATION_COMMANDS:
146
- return self.enforce_mutation(command_path)
147
-
148
- logger.warning(
149
- "clickstack_unknown_command_classification",
150
- command=command_path,
151
- )
152
- return EnforcementResult.deny(
153
- reason_code="unknown_command",
154
- explanation=f"Command '{command_path}' is not classified as read or mutation",
155
- )
156
-
157
-
158
- _adapter: ClickStackSurfaceAdapter | None = None
159
-
160
-
161
- def get_adapter() -> ClickStackSurfaceAdapter:
162
- """Get the singleton ClickStack surface adapter."""
163
- global _adapter
164
- if _adapter is None:
165
- _adapter = ClickStackSurfaceAdapter()
166
- return _adapter
@@ -1,49 +0,0 @@
1
- """CLI plugin for ClickStack commands.
2
-
3
- This module defines the ClickStackCliPlugin class which registers CLI commands
4
- for interacting with the ClickStack ClickHouse service.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import click
10
-
11
- from phlo.plugins.base import CliCommandPlugin, PluginMetadata
12
-
13
- from phlo_clickstack.cli import clickstack_group
14
-
15
-
16
- class ClickStackCliPlugin(CliCommandPlugin):
17
- """Register ClickStack CLI commands.
18
-
19
- This plugin provides CLI commands for querying and managing the
20
- ClickStack ClickHouse service instance.
21
-
22
- Example:
23
- Registered automatically when phlo_clickstack is installed.
24
- Use `phlo clickstack --help` to see available commands.
25
-
26
- """
27
-
28
- @property
29
- def metadata(self) -> PluginMetadata:
30
- """Return plugin metadata for ClickStack CLI registration.
31
-
32
- Returns:
33
- PluginMetadata: Metadata including name, version, and description.
34
-
35
- """
36
- return PluginMetadata(
37
- name="clickstack",
38
- version="0.1.0",
39
- description="CLI commands for ClickStack query access",
40
- )
41
-
42
- def get_cli_commands(self) -> list[click.Command]:
43
- """Return ClickStack CLI commands.
44
-
45
- Returns:
46
- list[click.Command]: List of Click commands provided by this plugin.
47
-
48
- """
49
- return [clickstack_group]
@@ -1,44 +0,0 @@
1
- """ClickStack service plugin.
2
-
3
- This module defines the ClickStackServicePlugin class which provides
4
- service registration and definition for the ClickStack observability
5
- backend service.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from phlo.plugins import PackageYamlServicePlugin, PluginMetadata
11
-
12
-
13
- class ClickStackServicePlugin(PackageYamlServicePlugin):
14
- """Service plugin for ClickStack.
15
-
16
- Provides ClickStack (ClickHouse-based observability backend) as a
17
- managed service within the Phlo services framework. The service
18
- definition is loaded from the bundled service.yaml file.
19
-
20
- Example:
21
- Plugin is automatically discovered via entry points.
22
- Service is started with `phlo services start clickstack`.
23
-
24
- Attributes:
25
- service_definition: ClickStack Docker Compose configuration.
26
-
27
- """
28
-
29
- @property
30
- def metadata(self) -> PluginMetadata:
31
- """Return plugin metadata for ClickStack service registration.
32
-
33
- Returns:
34
- PluginMetadata: Metadata including name, version, description,
35
- author, and tags for discovery.
36
-
37
- """
38
- return PluginMetadata(
39
- name="clickstack",
40
- version="0.1.0",
41
- description="ClickStack all-in-one observability backend",
42
- author="Phlo Team",
43
- tags=["observability", "logs", "metrics", "traces"],
44
- )