scim2-cli 0.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: SCIM application development CLI
5
5
  Keywords: scim,scim2,provisioning,rfc7643,rfc7644
6
6
  Author: Yaal Coop
@@ -208,7 +208,6 @@ License: Apache License
208
208
  limitations under the License.
209
209
  Classifier: Intended Audience :: Developers
210
210
  Classifier: Development Status :: 3 - Alpha
211
- Classifier: Programming Language :: Python :: 3.10
212
211
  Classifier: Programming Language :: Python :: 3.11
213
212
  Classifier: Programming Language :: Python :: 3.12
214
213
  Classifier: Programming Language :: Python :: 3.13
@@ -219,12 +218,12 @@ Classifier: Environment :: Web Environment
219
218
  Classifier: Programming Language :: Python
220
219
  Classifier: Operating System :: OS Independent
221
220
  Requires-Dist: click>=8.1.7
222
- Requires-Dist: scim2-client[httpx2]>=0.8.0
223
- Requires-Dist: scim2-tester>=0.3.0
221
+ Requires-Dist: scim2-client[httpx2]>=0.9.0
222
+ Requires-Dist: scim2-tester>=0.4.0
224
223
  Requires-Dist: sphinx-click-rst-to-ansi-formatter>=0.1.0
225
224
  Requires-Dist: pydanclick>=0.5.0
226
225
  Requires-Dist: pygments>=2.18.0
227
- Requires-Python: >=3.10
226
+ Requires-Python: >=3.11
228
227
  Project-URL: documentation, https://scim2-cli.readthedocs.io
229
228
  Project-URL: repository, https://github.com/python-scim/scim2-cli
230
229
  Project-URL: changelog, https://scim2-cli.readthedocs.io/en/latest/changelog.html
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.12.16,<0.13.0"]
2
+ requires = ["uv_build>=0.12.13,<0.13.0"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "SCIM application development CLI"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -17,7 +17,6 @@ keywords = [
17
17
  classifiers = [
18
18
  "Intended Audience :: Developers",
19
19
  "Development Status :: 3 - Alpha",
20
- "Programming Language :: Python :: 3.10",
21
20
  "Programming Language :: Python :: 3.11",
22
21
  "Programming Language :: Python :: 3.12",
23
22
  "Programming Language :: Python :: 3.13",
@@ -28,11 +27,11 @@ classifiers = [
28
27
  "Programming Language :: Python",
29
28
  "Operating System :: OS Independent",
30
29
  ]
31
- requires-python = ">= 3.10"
30
+ requires-python = ">= 3.11"
32
31
  dependencies = [
33
32
  "click>=8.1.7",
34
- "scim2-client[httpx2]>=0.8.0",
35
- "scim2-tester>=0.3.0",
33
+ "scim2-client[httpx2]>=0.9.0",
34
+ "scim2-tester>=0.4.0",
36
35
  "sphinx-click-rst-to-ansi-formatter>=0.1.0",
37
36
  "pydanclick>=0.5.0",
38
37
  "pygments>=2.18.0",
@@ -60,7 +59,7 @@ dev = [
60
59
  "mypy>=1.13.0",
61
60
  "prek>=0.1.0",
62
61
  "pytest>=8.2.1",
63
- "pytest-coverage>=0.0",
62
+ "pytest-cov>=6.0.0",
64
63
  "pytest-httpserver>=1.0.10",
65
64
  "tox-uv>=1.16.0",
66
65
  ]
@@ -76,6 +75,17 @@ doc = [
76
75
  ]
77
76
  bundle = ["pyinstaller>=6.10.0"]
78
77
 
78
+ [tool.uv]
79
+ exclude-newer = "14 days"
80
+
81
+ [tool.uv.exclude-newer-package]
82
+ scim2-models = false
83
+ scim2-client = false
84
+ scim2-server = false
85
+ scim2-cli = false
86
+ scim2-tester = false
87
+ pytest-scim2-server = false
88
+
79
89
  [tool.uv.build-backend]
80
90
  module-root = ""
81
91
 
@@ -139,7 +149,6 @@ testpaths = ["tests"]
139
149
  requires = ["tox>=4.19"]
140
150
  env_list = [
141
151
  "style",
142
- "py310",
143
152
  "py311",
144
153
  "py312",
145
154
  "py313",
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.12.16,<0.13.0"]
2
+ requires = ["uv_build>=0.12.13,<0.13.0"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "SCIM application development CLI"
9
9
  authors = [{name="Yaal Coop", email="contact@yaal.coop"}]
10
10
  license = {file = "LICENSE.md"}
@@ -13,7 +13,6 @@ keywords = ["scim", "scim2", "provisioning", "rfc7643", "rfc7644"]
13
13
  classifiers = [
14
14
  "Intended Audience :: Developers",
15
15
  "Development Status :: 3 - Alpha",
16
- "Programming Language :: Python :: 3.10",
17
16
  "Programming Language :: Python :: 3.11",
18
17
  "Programming Language :: Python :: 3.12",
19
18
  "Programming Language :: Python :: 3.13",
@@ -25,11 +24,11 @@ classifiers = [
25
24
  "Operating System :: OS Independent",
26
25
  ]
27
26
 
28
- requires-python = ">= 3.10"
27
+ requires-python = ">= 3.11"
29
28
  dependencies = [
30
29
  "click>=8.1.7",
31
- "scim2-client[httpx2]>=0.8.0",
32
- "scim2-tester>=0.3.0",
30
+ "scim2-client[httpx2]>=0.9.0",
31
+ "scim2-tester>=0.4.0",
33
32
  "sphinx-click-rst-to-ansi-formatter>=0.1.0",
34
33
  "pydanclick>=0.5.0",
35
34
  "pygments>=2.18.0",
@@ -46,7 +45,7 @@ dev = [
46
45
  "mypy>=1.13.0",
47
46
  "prek>=0.1.0",
48
47
  "pytest>=8.2.1",
49
- "pytest-coverage>=0.0",
48
+ "pytest-cov>=6.0.0",
50
49
  "pytest-httpserver>=1.0.10",
51
50
  "tox-uv>=1.16.0",
52
51
  ]
@@ -68,6 +67,10 @@ bundle = [
68
67
  scim = "scim2_cli:cli"
69
68
  scim2 = "scim2_cli:cli"
70
69
 
70
+ [tool.uv]
71
+ exclude-newer = "14 days"
72
+ exclude-newer-package = { scim2-models = false, scim2-client = false, scim2-server = false, scim2-cli = false, scim2-tester = false, pytest-scim2-server = false }
73
+
71
74
  [tool.uv.build-backend]
72
75
  module-root = ""
73
76
 
@@ -133,7 +136,6 @@ testpaths = ["tests"]
133
136
  requires = ["tox>=4.19"]
134
137
  env_list = [
135
138
  "style",
136
- "py310",
137
139
  "py311",
138
140
  "py312",
139
141
  "py313",
@@ -4,13 +4,12 @@ import re
4
4
  import click
5
5
  from httpx2 import Client
6
6
  from scim2_client.engines.httpx2 import SyncSCIMClient
7
- from scim2_models import Group
8
7
  from scim2_models import ListResponse
9
- from scim2_models import Resource
10
8
  from scim2_models import ResourceType
11
9
  from scim2_models import Schema
10
+ from scim2_models import ScimProvider
11
+ from scim2_models import ScimProviderError
12
12
  from scim2_models import ServiceProviderConfig
13
- from scim2_models import User
14
13
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
15
14
 
16
15
  from scim2_cli.create import create_cli
@@ -22,48 +21,39 @@ from scim2_cli.test import test_cli
22
21
  from scim2_cli.utils import DOC_URL
23
22
  from scim2_cli.utils import SCIM_EXCEPTIONS
24
23
  from scim2_cli.utils import HeaderType
24
+ from scim2_cli.utils import escape_control_characters
25
25
  from scim2_cli.utils import exception_to_click_error
26
26
  from scim2_cli.utils import split_headers
27
27
 
28
28
 
29
- def load_config_files(
30
- schemas_fd, resource_types_fd, service_provider_config_fd
31
- ) -> tuple[
32
- list[type[Resource]], list[ResourceType] | None, ServiceProviderConfig | None
33
- ]:
34
- if schemas_fd:
35
- schemas_payload = json.load(schemas_fd)
36
- if isinstance(schemas_payload, dict):
37
- schemas_obj = ListResponse[Schema].model_validate(schemas_payload).resources
38
- else:
39
- schemas_obj = [Schema.model_validate(schema) for schema in schemas_payload]
40
- resource_models = [Resource.from_schema(schema) for schema in schemas_obj]
41
-
42
- else:
43
- resource_models = [User, Group]
44
-
45
- if resource_types_fd:
46
- resource_types_payload = json.load(resource_types_fd)
47
- if isinstance(schemas_payload, dict):
48
- resource_types = (
49
- ListResponse[ResourceType]
50
- .model_validate(resource_types_payload)
51
- .resources
52
- )
53
- else:
54
- resource_types = [
55
- ResourceType.model_validate(item) for item in resource_types_payload
56
- ]
57
- else:
58
- resource_types = None
59
-
60
- if service_provider_config_fd:
61
- spc_payload = json.load(service_provider_config_fd)
62
- service_provider_config = ServiceProviderConfig.model_validate(spc_payload)
63
- else:
64
- service_provider_config = None
65
-
66
- return resource_models, resource_types, service_provider_config
29
+ def load_objects(fd, model):
30
+ """Read a list of objects, bare or wrapped in a ListResponse, from a JSON file."""
31
+ payload = json.load(fd)
32
+ if isinstance(payload, dict):
33
+ return ListResponse[model].model_validate(payload).resources or []
34
+ return [model.model_validate(item) for item in payload]
35
+
36
+
37
+ def describe_server(
38
+ scim_client, schemas_fd, resource_types_fd, service_provider_config_fd
39
+ ) -> ScimProvider:
40
+ """Describe the server with the configuration files, and query it for the others."""
41
+ resource_types = (
42
+ load_objects(resource_types_fd, ResourceType)
43
+ if resource_types_fd
44
+ else scim_client.query(ResourceType).resources or []
45
+ )
46
+ schemas = (
47
+ load_objects(schemas_fd, Schema)
48
+ if schemas_fd
49
+ else scim_client.query(Schema).resources or []
50
+ )
51
+ config = (
52
+ ServiceProviderConfig.model_validate(json.load(service_provider_config_fd))
53
+ if service_provider_config_fd
54
+ else scim_client.query(ServiceProviderConfig)
55
+ )
56
+ return ScimProvider.from_discovery(schemas, resource_types, config)
67
57
 
68
58
 
69
59
  @click.group(cls=make_rst_to_ansi_formatter(DOC_URL, group=True))
@@ -73,7 +63,7 @@ def load_config_files(
73
63
  "--header",
74
64
  multiple=True,
75
65
  type=HeaderType(),
76
- help="Headers to pass in the HTTP requests. Can be passed multiple times.",
66
+ help="Headers to pass in the HTTP requests. Can be passed multiple times. Other users of the machine can see the command line arguments, so pass the secrets with the SCIM_CLI_HEADERS environment variable.",
77
67
  envvar="SCIM_CLI_HEADERS",
78
68
  )
79
69
  @click.option(
@@ -122,29 +112,21 @@ def cli(
122
112
  headers_dict = split_headers(header)
123
113
  client = Client(base_url=url, headers=headers_dict, verify=not no_verify)
124
114
 
125
- resource_models, resource_types_obj, spc_obj = load_config_files(
126
- schemas, resource_types, service_provider_config
127
- )
128
-
129
- scim_client = SyncSCIMClient(
130
- client,
131
- resource_models=resource_models,
132
- resource_types=resource_types_obj,
133
- service_provider_config=spc_obj,
134
- )
115
+ scim_client = SyncSCIMClient(client)
135
116
  try:
136
- scim_client.discover(
137
- schemas=not bool(schemas),
138
- resource_types=not bool(resource_types),
139
- service_provider_config=not bool(service_provider_config),
117
+ scim_client.provider = describe_server(
118
+ scim_client, schemas, resource_types, service_provider_config
140
119
  )
141
- except SCIM_EXCEPTIONS as exc:
120
+ except (*SCIM_EXCEPTIONS, ScimProviderError) as exc:
142
121
  raise exception_to_click_error(exc) from exc
143
122
 
123
+ provider = scim_client.provider
144
124
  ctx.obj["client"] = scim_client
145
125
  ctx.obj["resource_models"] = {
146
- re.sub(r"\[.*\]", "", resource_model.__name__.lower()): resource_model
147
- for resource_model in ctx.obj["client"].resource_models
126
+ escape_control_characters(
127
+ re.sub(r"\[.*\]", "", resource_model.__name__.lower())
128
+ ): resource_model
129
+ for resource_model in map(provider.model_for, provider.resource_types)
148
130
  }
149
131
 
150
132
  if not click.get_text_stream("stdin").isatty(): # pragma: no cover
@@ -7,6 +7,7 @@ from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
7
  from scim2_cli.utils import DOC_URL
8
8
  from scim2_cli.utils import SCIM_EXCEPTIONS
9
9
  from scim2_cli.utils import ModelCommand
10
+ from scim2_cli.utils import escape_options_help
10
11
  from scim2_cli.utils import exception_to_click_error
11
12
  from scim2_cli.utils import formatted_payload
12
13
  from scim2_cli.utils import unacceptable_fields
@@ -75,7 +76,7 @@ def create_factory(model):
75
76
 
76
77
  create_payload(ctx.obj["client"], payload, indent)
77
78
 
78
- return create_command
79
+ return escape_options_help(create_command)
79
80
 
80
81
 
81
82
  @click.command(
@@ -4,6 +4,7 @@ from pydanclick import from_pydantic
4
4
  from scim2_models import Context
5
5
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
6
6
 
7
+ from scim2_cli.utils import escape_options_help
7
8
  from scim2_cli.utils import exception_to_click_error
8
9
 
9
10
  from .utils import DOC_URL
@@ -82,7 +83,7 @@ def replace_factory(model):
82
83
  indent,
83
84
  )
84
85
 
85
- return replace_command
86
+ return escape_options_help(replace_command)
86
87
 
87
88
 
88
89
  @click.command(
@@ -7,6 +7,7 @@ from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
7
 
8
8
  from .utils import DOC_URL
9
9
  from .utils import Color
10
+ from .utils import escape_control_characters
10
11
 
11
12
 
12
13
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="test")
@@ -43,11 +44,11 @@ def test_cli(ctx, verbose, check_status_code, check_content_type):
43
44
  else:
44
45
  status = click.style(result.status.name, fg=Color.red)
45
46
  success = False
46
- click.echo(f"{status} {result.title}")
47
+ click.echo(f"{status} {escape_control_characters(result.title)}")
47
48
 
48
49
  if result.reason:
49
- click.echo(f" {result.reason}")
50
+ click.echo(f" {escape_control_characters(result.reason)}")
50
51
  if verbose and result.data:
51
- click.echo(f" {result.data}")
52
+ click.echo(f" {escape_control_characters(result.data)}")
52
53
 
53
54
  sys.exit(0 if success else 1)
@@ -1,6 +1,6 @@
1
1
  import json
2
- import sys
3
- from enum import Enum
2
+ import re
3
+ from enum import StrEnum
4
4
 
5
5
  import click
6
6
  from scim2_client import SCIMClientException
@@ -12,13 +12,17 @@ INDENTATION_SIZE = 4
12
12
 
13
13
  SCIM_EXCEPTIONS = (SCIMClientException, SCIMException)
14
14
 
15
+ CONTROL_CHARACTERS = re.compile(
16
+ "[\x00-\x08\x0b-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]"
17
+ )
18
+
15
19
 
16
20
  class HeaderType(click.ParamType):
17
21
  envvar_list_splitter = ";"
18
22
  name = "HEADER"
19
23
 
20
24
 
21
- class Color(str, Enum):
25
+ class Color(StrEnum):
22
26
  black = "black"
23
27
  red = "red"
24
28
  green = "green"
@@ -37,6 +41,25 @@ class Color(str, Enum):
37
41
  bright_white = "bright_white"
38
42
 
39
43
 
44
+ def escape_control_characters(text) -> str:
45
+ """Replace the control characters of a text by their escaped form.
46
+
47
+ The texts coming from the server would otherwise be interpreted by the
48
+ terminal as escape sequences.
49
+ """
50
+ return CONTROL_CHARACTERS.sub(
51
+ lambda match: match.group().encode("unicode_escape").decode(), str(text)
52
+ )
53
+
54
+
55
+ def escape_options_help(command):
56
+ """Escape the option help texts pydanclick takes from the schema descriptions."""
57
+ for param in command.params:
58
+ if param.help:
59
+ param.help = escape_control_characters(param.help)
60
+ return command
61
+
62
+
40
63
  def formatted_payload(obj, indent):
41
64
  indent = INDENTATION_SIZE if indent else False
42
65
  return json.dumps(obj, indent=indent)
@@ -115,8 +138,6 @@ def unacceptable_fields(context, model):
115
138
 
116
139
  def exception_to_click_error(exception):
117
140
  message = str(exception)
118
- if sys.version_info >= (3, 11) and hasattr(
119
- exception, "__notes__"
120
- ): # pragma: no cover
141
+ if hasattr(exception, "__notes__"):
121
142
  message += "\n" + "\n".join(exception.__notes__)
122
- return click.ClickException(message)
143
+ return click.ClickException(escape_control_characters(message))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes