midpoint-cli 1.2.0__tar.gz → 1.3.1__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 (41) hide show
  1. midpoint_cli-1.3.1/MANIFEST.in +1 -0
  2. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/PKG-INFO +4 -1
  3. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/README.md +3 -0
  4. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/PKG-INFO +4 -1
  5. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/SOURCES.txt +2 -0
  6. midpoint_cli-1.3.1/requirements.txt +7 -0
  7. midpoint_cli-1.3.1/src/midpoint_cli/__init__.py +1 -0
  8. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/__init__.py +25 -9
  9. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/objects.py +73 -3
  10. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/task.py +30 -4
  11. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/user.py +22 -2
  12. midpoint_cli-1.2.0/src/midpoint_cli/__init__.py +0 -1
  13. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/LICENSE +0 -0
  14. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/dependency_links.txt +0 -0
  15. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/requires.txt +0 -0
  16. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/top_level.txt +0 -0
  17. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/setup.cfg +0 -0
  18. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/setup.py +0 -0
  19. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint-cli +0 -0
  20. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/observer.py +0 -0
  21. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/patch.py +0 -0
  22. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/progress.py +0 -0
  23. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/session.py +0 -0
  24. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/__init__.py +0 -0
  25. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/base.py +0 -0
  26. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/complete.py +0 -0
  27. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/configuration.py +0 -0
  28. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/console.py +0 -0
  29. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/delete.py +0 -0
  30. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/get.py +0 -0
  31. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/org.py +0 -0
  32. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/put.py +0 -0
  33. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/resource.py +0 -0
  34. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/script.py +0 -0
  35. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_client_api.py +0 -0
  36. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_client_model.py +0 -0
  37. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_client_put.py +0 -0
  38. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_environment.py +0 -0
  39. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_parser.py +0 -0
  40. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_patch.py +0 -0
  41. {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_script.py +0 -0
@@ -0,0 +1 @@
1
+ include requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: midpoint-cli
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: A command line client to Midpoint Identity Management system.
5
5
  Home-page: https://gitlab.com/alcibiade/midpoint-cli
6
6
  Author: Yannick Kirschhoffer
@@ -21,6 +21,8 @@ Requires-Dist: requests>=2.31
21
21
  Requires-Dist: urllib3>=2.0
22
22
  Requires-Dist: setuptools>=68.0
23
23
 
24
+ ![link=https://badge.fury.io/py/midpoint-cli](https://badge.fury.io/py/midpoint-cli.svg)
25
+
24
26
  ## Midpoint CLI
25
27
 
26
28
  This project is a command line client interface used to drive an Evolveum Midpoint identity management server.
@@ -229,5 +231,6 @@ midpoint-1
229
231
 
230
232
  * Update revision in src/midpoint_cli/__init__.py
231
233
  * Commit and tag with corresponding version number
234
+ * Generate markdown documentation: downdoc README.adoc
232
235
  * Build distribution: python setup.py sdist
233
236
  * Upload distribution to PyPI: twine upload dist/*
@@ -1,3 +1,5 @@
1
+ ![link=https://badge.fury.io/py/midpoint-cli](https://badge.fury.io/py/midpoint-cli.svg)
2
+
1
3
  ## Midpoint CLI
2
4
 
3
5
  This project is a command line client interface used to drive an Evolveum Midpoint identity management server.
@@ -206,5 +208,6 @@ midpoint-1
206
208
 
207
209
  * Update revision in src/midpoint_cli/__init__.py
208
210
  * Commit and tag with corresponding version number
211
+ * Generate markdown documentation: downdoc README.adoc
209
212
  * Build distribution: python setup.py sdist
210
213
  * Upload distribution to PyPI: twine upload dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: midpoint-cli
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: A command line client to Midpoint Identity Management system.
5
5
  Home-page: https://gitlab.com/alcibiade/midpoint-cli
6
6
  Author: Yannick Kirschhoffer
@@ -21,6 +21,8 @@ Requires-Dist: requests>=2.31
21
21
  Requires-Dist: urllib3>=2.0
22
22
  Requires-Dist: setuptools>=68.0
23
23
 
24
+ ![link=https://badge.fury.io/py/midpoint-cli](https://badge.fury.io/py/midpoint-cli.svg)
25
+
24
26
  ## Midpoint CLI
25
27
 
26
28
  This project is a command line client interface used to drive an Evolveum Midpoint identity management server.
@@ -229,5 +231,6 @@ midpoint-1
229
231
 
230
232
  * Update revision in src/midpoint_cli/__init__.py
231
233
  * Commit and tag with corresponding version number
234
+ * Generate markdown documentation: downdoc README.adoc
232
235
  * Build distribution: python setup.py sdist
233
236
  * Upload distribution to PyPI: twine upload dist/*
@@ -1,5 +1,7 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
4
+ requirements.txt
3
5
  setup.cfg
4
6
  setup.py
5
7
  midpoint_cli.egg-info/PKG-INFO
@@ -0,0 +1,7 @@
1
+ clint>=0.5
2
+ tabulate>=0.9
3
+ unidecode>=1.3
4
+ requests>=2.31
5
+ urllib3>=2.0
6
+
7
+ setuptools>=68.0
@@ -0,0 +1 @@
1
+ __version__ = '1.3.1'
@@ -147,6 +147,18 @@ class MidpointClient:
147
147
  if api_client is not None:
148
148
  self.api_client = api_client
149
149
 
150
+ def __get_object(self, midpoint_type: str, oid: str) -> Element:
151
+ response = self.api_client.get_element(midpoint_type, oid)
152
+ tree = ElementTree.fromstring(response)
153
+
154
+ status = tree.find('c:status', namespaces)
155
+
156
+ if status is not None and status.text == 'fatal_error':
157
+ message = tree.find('c:partialResults/c:message', namespaces).text
158
+ raise MidpointServerError(message)
159
+
160
+ return tree
161
+
150
162
  def __get_collection(self, mp_class: str, local_class: Type) -> MidpointObjectList:
151
163
  tree = self.api_client.get_elements(mp_class)
152
164
  return MidpointObjectList([local_class(entity) for entity in tree])
@@ -160,6 +172,11 @@ class MidpointClient:
160
172
  def get_connectors(self) -> MidpointObjectList:
161
173
  return self.__get_collection('connector', MidpointConnector)
162
174
 
175
+ def get_user(self, oid: str) -> Optional[MidpointUser]:
176
+ user_root = self.__get_object('user', oid)
177
+ user = MidpointUser(user_root)
178
+ return user
179
+
163
180
  def get_users(self) -> MidpointObjectList:
164
181
  return self.__get_collection('user', MidpointUser)
165
182
 
@@ -171,18 +188,17 @@ class MidpointClient:
171
188
  selected_orgs = self._filter(queryterms, orgs)
172
189
  return selected_orgs
173
190
 
174
- def task_action(self, task_oid: str, task_action: str) -> None:
191
+ def task_action(self, task_oid: str, task_action: str) -> Optional[MidpointTask]:
175
192
  self.api_client.execute_action('task', task_oid, task_action)
176
193
 
177
194
  if task_action == 'run':
178
195
  return self.task_wait(task_oid)
179
196
 
180
- def task_wait(self, task_oid: str) -> None:
197
+ def task_wait(self, task_oid: str) -> MidpointTask:
181
198
  with AsciiProgressMonitor() as progress:
182
199
  while True:
183
200
  time.sleep(2)
184
- task_xml = self.api_client.get_element('task', task_oid)
185
- task_root = ElementTree.fromstring(task_xml)
201
+ task_root = self.__get_object('task', task_oid)
186
202
  task = MidpointTask(task_root)
187
203
 
188
204
  progress.update(int(task['Progress'] or '0'))
@@ -194,7 +210,7 @@ class MidpointClient:
194
210
  if rstatus != 'success':
195
211
  raise TaskExecutionFailure('Failed execution of task ' + task_oid + ' with status ' + rstatus)
196
212
 
197
- break
213
+ return task
198
214
 
199
215
  def test_resource(self, resource_oid: str) -> None:
200
216
  response = self.api_client.execute_action('resource', resource_oid, 'test')
@@ -202,11 +218,11 @@ class MidpointClient:
202
218
  status = tree.find('c:status', namespaces).text
203
219
  return status
204
220
 
205
- def get_xml(self, type: str, oid: str) -> Optional[str]:
206
- return self.api_client.get_element(type, oid)
221
+ def get_xml(self, midpoint_type: str, oid: str) -> Optional[str]:
222
+ return self.api_client.get_element(midpoint_type, oid)
207
223
 
208
224
  def put_xml(self, xml_file: str, patch_file: str = None, patch_write: bool = False) -> Tuple[str, str]:
209
225
  return self.api_client.put_element(xml_file, patch_file, patch_write)
210
226
 
211
- def delete(self, type: str, oid: str) -> str:
212
- return self.api_client.delete(type, oid)
227
+ def delete(self, midpoint_type: str, oid: str) -> str:
228
+ return self.api_client.delete(midpoint_type, oid)
@@ -1,9 +1,12 @@
1
+ import itertools
1
2
  import re
2
3
  from collections import OrderedDict
4
+ from textwrap import dedent
3
5
  from typing import Optional, List
4
6
  from xml.etree import ElementTree
5
7
  from xml.etree.ElementTree import Element
6
8
 
9
+ import tabulate
7
10
  from unidecode import unidecode
8
11
 
9
12
  namespaces = {
@@ -76,6 +79,62 @@ class MidpointTask(MidpointObject):
76
79
  total = xml_entity.find('c:expectedTotal', namespaces)
77
80
  self['Expected Total'] = total.text if total is not None else ''
78
81
 
82
+ # Collect execution statistics
83
+
84
+ self._transitions = []
85
+ self._actions = []
86
+
87
+ statistics = xml_entity.find('c:activityState/c:activity/c:statistics', namespaces)
88
+
89
+ if statistics is not None:
90
+ synchronization = statistics.find('c:synchronization', namespaces)
91
+
92
+ for transition in synchronization or []:
93
+ if transition.find('c:onSynchronizationStart', namespaces) is not None:
94
+ state_start = transition.find('c:onSynchronizationStart', namespaces).text
95
+ state_end = transition.find('c:onSynchronizationEnd', namespaces).text
96
+ count = int(transition.find('c:counter/c:count', namespaces).text)
97
+ self._transitions.append((state_start, state_end, count))
98
+
99
+ if transition.find('c:exclusionReason', namespaces) is not None:
100
+ reason = transition.find('c:exclusionReason', namespaces).text
101
+ count = int(transition.find('c:counter/c:count', namespaces).text)
102
+ self._transitions.append((reason, '', count))
103
+
104
+ actions_executed = statistics.find('c:actionsExecuted', namespaces)
105
+
106
+ for object_actions_entry in actions_executed or []:
107
+ object_type = object_actions_entry.find('c:objectType', namespaces).text
108
+ operation = object_actions_entry.find('c:operation', namespaces).text
109
+ count_success = int(object_actions_entry.find('c:totalSuccessCount', namespaces).text)
110
+ count_failure = int(object_actions_entry.find('c:totalFailureCount', namespaces).text)
111
+
112
+ self._actions.append((object_type, operation, count_success, count_failure))
113
+
114
+ def get_transitions(self):
115
+ return self._transitions
116
+
117
+ def get_actions(self):
118
+ return self._actions
119
+
120
+ def get_full_description(self):
121
+ text = dedent("""\
122
+ {}
123
+
124
+ Transitions
125
+ {}
126
+
127
+ Actions
128
+ {}
129
+ """).format(tabulate.tabulate(self.items()),
130
+ tabulate.tabulate(self.get_transitions(),
131
+ headers=['From State', 'To State', 'Count']),
132
+ tabulate.tabulate(self.get_actions(),
133
+ headers=['Type', 'Action', 'Count Success', 'Count Failure'])
134
+ )
135
+
136
+ return text
137
+
79
138
 
80
139
  class MidpointResource(MidpointObject):
81
140
 
@@ -99,6 +158,10 @@ class MidpointConnector(MidpointObject):
99
158
 
100
159
  class MidpointUser(MidpointObject):
101
160
 
161
+ @staticmethod
162
+ def strip_namespace(tag: str) -> str:
163
+ return re.sub(r'{.*}', '', tag)
164
+
102
165
  def __init__(self, xml_entity: Element):
103
166
  super().__init__()
104
167
  self['OID'] = xml_entity.attrib['oid']
@@ -110,11 +173,18 @@ class MidpointUser(MidpointObject):
110
173
  self['Email'] = optional_text(xml_entity.find('c:emailAddress', namespaces))
111
174
  self['OU'] = optional_text(xml_entity.find('c:organizationalUnit', namespaces))
112
175
 
176
+ activation = xml_entity.find('c:activation', namespaces)
113
177
  extfields = xml_entity.find('c:extension', namespaces)
114
178
 
115
- if extfields is not None:
116
- for extfield in extfields:
117
- self[re.sub(r'{.*}', '', extfield.tag)] = extfield.text
179
+ self._all_attributes = []
180
+
181
+ for child in itertools.chain(xml_entity, activation, extfields or []):
182
+ # Check if the child has no children and contains non-empty text
183
+ if len(child) == 0 and (child.text is not None and child.text.strip()):
184
+ self._all_attributes.append((self.strip_namespace(child.tag), child.text))
185
+
186
+ def get_all_attributes(self):
187
+ return self._all_attributes
118
188
 
119
189
 
120
190
  class MidpointOrganization(MidpointObject):
@@ -4,7 +4,7 @@ from argparse import ArgumentParser, RawTextHelpFormatter
4
4
  import tabulate
5
5
 
6
6
  # Task command wrapper parser
7
- from midpoint_cli.client import TaskExecutionFailure
7
+ from midpoint_cli.client import TaskExecutionFailure, MidpointTask
8
8
  from midpoint_cli.prompt.base import PromptBase
9
9
 
10
10
  task_parser = ArgumentParser(
@@ -40,6 +40,15 @@ task_wait_parser = ArgumentParser(
40
40
  )
41
41
  task_wait_parser.add_argument('task', help='Task to wait for. Can be an OID or a task name.', nargs='*')
42
42
 
43
+ # Task GET parser
44
+
45
+ task_get_parser = ArgumentParser(
46
+ formatter_class=RawTextHelpFormatter,
47
+ prog='task get',
48
+ description='Get data about a task.',
49
+ )
50
+ task_get_parser.add_argument('task', help='Task to wait for. Can be an OID or a task name.', nargs='*')
51
+
43
52
 
44
53
  class TaskClientPrompt(PromptBase):
45
54
 
@@ -51,6 +60,19 @@ class TaskClientPrompt(PromptBase):
51
60
  if ns.command == 'ls':
52
61
  tasks = self.client.get_tasks()
53
62
  print(tabulate.tabulate(tasks, headers='keys'))
63
+ elif ns.command == 'get':
64
+ tasks = self.client.get_tasks()
65
+ get_ns = task_wait_parser.parse_args(task_args[1:])
66
+
67
+ tasks_to_get = get_ns.task
68
+
69
+ for task_id in tasks_to_get:
70
+ task_obj: MidpointTask = tasks.find_object(task_id)
71
+ if task_obj is None:
72
+ print(f'Task reference not found: {task_id}')
73
+ else:
74
+ print(task_obj.get_full_description())
75
+
54
76
  elif ns.command == 'wait':
55
77
  tasks = self.client.get_tasks()
56
78
  wait_ns = task_wait_parser.parse_args(task_args[1:])
@@ -63,9 +85,9 @@ class TaskClientPrompt(PromptBase):
63
85
  for task_id in tasks_to_wait:
64
86
  task_obj = tasks.find_object(task_id)
65
87
  if task_obj is None:
66
- print('Task reference not found:', task_id)
88
+ print(f'Task reference not found: {task_id}')
67
89
  else:
68
- print('Waiting for task', task_obj.get_oid(), '/', task_obj.get_name())
90
+ print(f'Waiting for task {task_obj.get_oid()} / {task_obj.get_name()}')
69
91
  self.client.task_wait(task_obj.get_oid())
70
92
 
71
93
  elif ns.command in ['run', 'resume', 'suspend']:
@@ -85,11 +107,15 @@ class TaskClientPrompt(PromptBase):
85
107
 
86
108
  print('Task', task_obj.get_name(), '-', ns.command)
87
109
  try:
88
- self.client.task_action(task_obj.get_oid(), ns.command)
110
+ task_result = self.client.task_action(task_obj.get_oid(), ns.command)
111
+
112
+ if task_result is not None:
113
+ print(task_result.get_full_description())
89
114
  except TaskExecutionFailure as e:
90
115
  self.error_code = 1
91
116
  self.error_message = e.message
92
117
  break
118
+
93
119
  else:
94
120
  self.error_code = 1
95
121
  self.error_message = f'Unknown command {ns.command}'
@@ -3,6 +3,7 @@ from argparse import ArgumentParser, RawTextHelpFormatter
3
3
 
4
4
  import tabulate
5
5
 
6
+ from midpoint_cli.client import MidpointUser, MidpointObjectList, MidpointServerError
6
7
  # User command wrapper parser
7
8
  from midpoint_cli.prompt.base import PromptBase
8
9
 
@@ -23,10 +24,17 @@ user_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
23
24
  user_search_parser = ArgumentParser(
24
25
  formatter_class=RawTextHelpFormatter,
25
26
  prog='user search',
26
- description='Search for a user.',
27
+ description='Search for users by substring.',
27
28
  )
28
29
  user_search_parser.add_argument('searchquery', help='A string fragment found in the user data.', nargs='+')
29
30
 
31
+ user_get_parser = ArgumentParser(
32
+ formatter_class=RawTextHelpFormatter,
33
+ prog='user get',
34
+ description='Search for users by OID.',
35
+ )
36
+ user_get_parser.add_argument('oid', help='An OID or name value.')
37
+
30
38
 
31
39
  class UserClientPrompt(PromptBase):
32
40
 
@@ -42,6 +50,14 @@ class UserClientPrompt(PromptBase):
42
50
  search_ns = user_search_parser.parse_args(user_args[1:])
43
51
  users = self.client.get_users().filter(search_ns.searchquery)
44
52
  self.print_users(users)
53
+ elif ns.command == 'get':
54
+ get_ns = user_get_parser.parse_args(user_args[1:])
55
+ try:
56
+ user = self.client.get_user(get_ns.oid)
57
+ self.print_user(user)
58
+ except MidpointServerError as e:
59
+ self.error_code = 1
60
+ self.error_message = str(e)
45
61
  else:
46
62
  self.error_code = 1
47
63
  self.error_message = f'Unknown command {ns.command}'
@@ -50,7 +66,11 @@ class UserClientPrompt(PromptBase):
50
66
  pass
51
67
 
52
68
  @staticmethod
53
- def print_users(users):
69
+ def print_user(user: MidpointUser):
70
+ print(tabulate.tabulate(user.get_all_attributes()))
71
+
72
+ @staticmethod
73
+ def print_users(users: MidpointObjectList):
54
74
  print(tabulate.tabulate(users, headers='keys'))
55
75
  print()
56
76
  print(f'Total: {len(users)} users')
@@ -1 +0,0 @@
1
- __version__ = '1.2.0'
File without changes
File without changes
File without changes