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.
- midpoint_cli-1.3.1/MANIFEST.in +1 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/PKG-INFO +4 -1
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/README.md +3 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/PKG-INFO +4 -1
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/SOURCES.txt +2 -0
- midpoint_cli-1.3.1/requirements.txt +7 -0
- midpoint_cli-1.3.1/src/midpoint_cli/__init__.py +1 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/__init__.py +25 -9
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/objects.py +73 -3
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/task.py +30 -4
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/user.py +22 -2
- midpoint_cli-1.2.0/src/midpoint_cli/__init__.py +0 -1
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/LICENSE +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/dependency_links.txt +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/requires.txt +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/midpoint_cli.egg-info/top_level.txt +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/setup.cfg +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/setup.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint-cli +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/observer.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/patch.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/progress.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/client/session.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/__init__.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/base.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/complete.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/configuration.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/console.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/delete.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/get.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/org.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/put.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/resource.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/src/midpoint_cli/prompt/script.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_client_api.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_client_model.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_client_put.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_environment.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_parser.py +0 -0
- {midpoint_cli-1.2.0 → midpoint_cli-1.3.1}/test/test_patch.py +0 -0
- {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.
|
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
|
+

|
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
|
+

|
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.
|
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
|
+

|
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/*
|
@@ -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) ->
|
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) ->
|
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
|
-
|
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
|
-
|
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,
|
206
|
-
return self.api_client.get_element(
|
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,
|
212
|
-
return self.api_client.delete(
|
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
|
-
|
116
|
-
|
117
|
-
|
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:
|
88
|
+
print(f'Task reference not found: {task_id}')
|
67
89
|
else:
|
68
|
-
print('Waiting for task
|
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|