cli2 4.3.4__tar.gz → 5.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.
Files changed (60) hide show
  1. {cli2-4.3.4/cli2.egg-info → cli2-5.0.0}/PKG-INFO +5 -4
  2. {cli2-4.3.4 → cli2-5.0.0}/cli2/__init__.py +14 -26
  3. cli2-5.0.0/cli2/asyncio.py +130 -0
  4. {cli2-4.3.4 → cli2-5.0.0}/cli2/display.py +1 -6
  5. {cli2-4.3.4 → cli2-5.0.0}/cli2/mask.py +3 -1
  6. {cli2-4.3.4 → cli2-5.0.0/cli2.egg-info}/PKG-INFO +5 -4
  7. {cli2-4.3.4 → cli2-5.0.0}/cli2.egg-info/SOURCES.txt +0 -6
  8. {cli2-4.3.4 → cli2-5.0.0}/cli2.egg-info/requires.txt +5 -3
  9. {cli2-4.3.4 → cli2-5.0.0}/setup.py +3 -5
  10. {cli2-4.3.4 → cli2-5.0.0}/tests/test_ansible.py +75 -20
  11. {cli2-4.3.4 → cli2-5.0.0}/tests/test_ansible_variables.py +19 -12
  12. cli2-5.0.0/tests/test_asyncio.py +68 -0
  13. {cli2-4.3.4 → cli2-5.0.0}/tests/test_cli.py +1 -1
  14. {cli2-4.3.4 → cli2-5.0.0}/tests/test_client.py +102 -67
  15. {cli2-4.3.4 → cli2-5.0.0}/tests/test_mask.py +5 -0
  16. cli2-4.3.4/cli2/ansible/__init__.py +0 -16
  17. cli2-4.3.4/cli2/ansible/action.py +0 -400
  18. cli2-4.3.4/cli2/ansible/playbook.py +0 -215
  19. cli2-4.3.4/cli2/ansible/variables.py +0 -115
  20. cli2-4.3.4/cli2/asyncio.py +0 -42
  21. cli2-4.3.4/cli2/client.py +0 -1870
  22. cli2-4.3.4/cli2/examples/client.py +0 -61
  23. cli2-4.3.4/tests/test_asyncio.py +0 -18
  24. {cli2-4.3.4 → cli2-5.0.0}/MANIFEST.in +0 -0
  25. {cli2-4.3.4 → cli2-5.0.0}/README.rst +0 -0
  26. {cli2-4.3.4 → cli2-5.0.0}/classifiers.txt +0 -0
  27. {cli2-4.3.4 → cli2-5.0.0}/cli2/cli.py +0 -0
  28. {cli2-4.3.4 → cli2-5.0.0}/cli2/cli2.py +0 -0
  29. {cli2-4.3.4 → cli2-5.0.0}/cli2/colors.py +0 -0
  30. {cli2-4.3.4 → cli2-5.0.0}/cli2/configuration.py +0 -0
  31. {cli2-4.3.4 → cli2-5.0.0}/cli2/decorators.py +0 -0
  32. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/__init__.py +0 -0
  33. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/conf.py +0 -0
  34. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/example.py +0 -0
  35. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/example_obj.py +0 -0
  36. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/nesting.py +0 -0
  37. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/obj.py +0 -0
  38. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/obj2.py +0 -0
  39. {cli2-4.3.4 → cli2-5.0.0}/cli2/examples/test.py +0 -0
  40. {cli2-4.3.4 → cli2-5.0.0}/cli2/lock.py +0 -0
  41. {cli2-4.3.4 → cli2-5.0.0}/cli2/log.py +0 -0
  42. {cli2-4.3.4 → cli2-5.0.0}/cli2/node.py +0 -0
  43. {cli2-4.3.4 → cli2-5.0.0}/cli2/sphinx.py +0 -0
  44. {cli2-4.3.4 → cli2-5.0.0}/cli2/table.py +0 -0
  45. {cli2-4.3.4 → cli2-5.0.0}/cli2/test.py +0 -0
  46. {cli2-4.3.4 → cli2-5.0.0}/cli2.egg-info/dependency_links.txt +0 -0
  47. {cli2-4.3.4 → cli2-5.0.0}/cli2.egg-info/entry_points.txt +0 -0
  48. {cli2-4.3.4 → cli2-5.0.0}/cli2.egg-info/top_level.txt +0 -0
  49. {cli2-4.3.4 → cli2-5.0.0}/setup.cfg +0 -0
  50. {cli2-4.3.4 → cli2-5.0.0}/tests/test_command.py +0 -0
  51. {cli2-4.3.4 → cli2-5.0.0}/tests/test_configuration.py +0 -0
  52. {cli2-4.3.4 → cli2-5.0.0}/tests/test_decorators.py +0 -0
  53. {cli2-4.3.4 → cli2-5.0.0}/tests/test_display.py +0 -0
  54. {cli2-4.3.4 → cli2-5.0.0}/tests/test_entry_point.py +0 -0
  55. {cli2-4.3.4 → cli2-5.0.0}/tests/test_group.py +0 -0
  56. {cli2-4.3.4 → cli2-5.0.0}/tests/test_inject.py +0 -0
  57. {cli2-4.3.4 → cli2-5.0.0}/tests/test_lock.py +0 -0
  58. {cli2-4.3.4 → cli2-5.0.0}/tests/test_node.py +0 -0
  59. {cli2-4.3.4 → cli2-5.0.0}/tests/test_restful.py +0 -0
  60. {cli2-4.3.4 → cli2-5.0.0}/tests/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.3.4
3
+ Version: 5.0.0
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -13,9 +13,10 @@ Requires-Dist: docstring_parser
13
13
  Requires-Dist: pyyaml
14
14
  Requires-Dist: pygments
15
15
  Requires-Dist: structlog
16
- Provides-Extra: client
17
- Requires-Dist: httpx; extra == "client"
18
- Requires-Dist: truststore; extra == "client"
16
+ Provides-Extra: httpx
17
+ Requires-Dist: chttpx; extra == "httpx"
18
+ Provides-Extra: ansible
19
+ Requires-Dist: cansible; extra == "ansible"
19
20
  Provides-Extra: test
20
21
  Requires-Dist: freezegun; extra == "test"
21
22
  Requires-Dist: pytest; extra == "test"
@@ -9,35 +9,10 @@ from .cli import (
9
9
  Group,
10
10
  EntryPoint,
11
11
  )
12
- from .asyncio import async_resolve, async_run
12
+ from .asyncio import async_resolve, async_run, Queue
13
13
  from .colors import colors as c
14
14
 
15
15
  from .configuration import Configuration, cfg
16
- try:
17
- from .client import (
18
- ClientError,
19
- ResponseError,
20
- TokenGetError,
21
- RefusedResponseError,
22
- RetriesExceededError,
23
- FieldError,
24
- FieldValueError,
25
- FieldExternalizeError,
26
- Client,
27
- ClientCommand,
28
- DateTimeField,
29
- Field,
30
- Handler,
31
- JSONStringField,
32
- Model,
33
- ModelCommand,
34
- Paginator,
35
- Related,
36
- )
37
- except ImportError:
38
- raise
39
- # httpx not installed
40
- pass
41
16
  from .display import diff, diff_data, render, print, highlight, yaml_highlight
42
17
  from .lock import Lock
43
18
  from .log import configure, log
@@ -55,3 +30,16 @@ def which(cmd):
55
30
  path = Path(os.getenv('HOME')) / '.local/bin' / cmd
56
31
  if path.exists():
57
32
  return str(path)
33
+
34
+
35
+ def mutable(obj):
36
+ types = (
37
+ int,
38
+ float,
39
+ str,
40
+ tuple,
41
+ frozenset,
42
+ bool,
43
+ bytes,
44
+ )
45
+ return not isinstance(obj, types)
@@ -0,0 +1,130 @@
1
+ import asyncio
2
+ import inspect
3
+ from . import display
4
+ from .log import log
5
+
6
+
7
+ def async_iter(obj):
8
+ """ Check if an object is an async iterable. """
9
+ return inspect.isasyncgen(obj) or hasattr(obj, '__aiter__')
10
+
11
+
12
+ async def async_resolve(result, output=False):
13
+ """
14
+ Recursively resolve awaitables and async iterables.
15
+
16
+ :param result: The awaitable or async iterable to resolve
17
+ :param output: If True, print results as they are resolved. If False,
18
+ collect results.
19
+
20
+ :return: The resolved value(s). If output is True, returns None. If output
21
+ is False, returns a list of resolved values from async iterables.
22
+ """
23
+ while inspect.iscoroutine(result):
24
+ result = await result
25
+ if async_iter(result):
26
+ results = []
27
+ async for _ in result:
28
+ if output:
29
+ if (
30
+ not inspect.iscoroutine(_)
31
+ and not inspect.isasyncgen(_)
32
+ ):
33
+ display.print(_)
34
+ else:
35
+ await async_resolve(_, output=output)
36
+ else:
37
+ results.append(await async_resolve(_))
38
+ return None if output else results
39
+ return result
40
+
41
+
42
+ def async_run(coroutine):
43
+ """Run an async coroutine in the current event loop or create a new one.
44
+
45
+ If an event loop is already running, creates a task in that loop.
46
+ If no event loop is running, creates a new one and runs the coroutine.
47
+
48
+ :param coroutine: The coroutine to run (return value of an async function)
49
+ :return: The result of the coroutine execution
50
+ """
51
+ try:
52
+ loop = asyncio.get_running_loop()
53
+ except RuntimeError:
54
+ return asyncio.run(coroutine)
55
+ else:
56
+ return loop.create_task(coroutine)
57
+
58
+
59
+ class Queue(asyncio.Queue):
60
+ """
61
+ An async queue with worker pool for concurrent task processing.
62
+
63
+ Extends asyncio.Queue to manage a pool of worker tasks that process items
64
+ from the queue concurrently.
65
+
66
+ .. code-block:: python
67
+
68
+ # will run 2 at the time
69
+ queue = cli2.Queue(num_workers=2)
70
+ # call like asyncio.run
71
+ await queue.run(foo(), bar(), other())
72
+
73
+ .. py:attribute:: num_workers
74
+
75
+ Number of concurrent workers (default: 12)
76
+
77
+ .. py:attribute:: results
78
+
79
+ List of results from completed tasks, order of results not garanteed
80
+ due to concurrency.
81
+ """
82
+
83
+ def __init__(self, *args, num_workers=12, **kwargs):
84
+ """Initialize the queue with worker pool.
85
+
86
+ :param num_workers: Number of concurrent workers (default: 12)
87
+ :paarm *args: Positional arguments for asyncio.Queue
88
+ :param **kwargs: Keyword arguments for asyncio.Queue
89
+ """
90
+ self.num_workers = num_workers or 12
91
+ self.results = []
92
+ super().__init__(*args, **kwargs)
93
+
94
+ async def run(self, *tasks):
95
+ """
96
+ Run tasks through the worker pool.
97
+
98
+ :param tasks: Coroutines
99
+ """
100
+ self.results = []
101
+
102
+ for task in tasks:
103
+ await self.put(task)
104
+
105
+ workers = [
106
+ asyncio.create_task(self.worker())
107
+ for i in range(self.num_workers)
108
+ ]
109
+
110
+ await self.join()
111
+ for worker in workers:
112
+ worker.cancel()
113
+
114
+ async def worker(self):
115
+ """Worker task that processes items from the queue.
116
+
117
+ Continuously gets tasks from the queue, executes them, and stores
118
+ results.
119
+ Handles exceptions by logging them.
120
+ """
121
+ while True:
122
+ task = await self.get()
123
+ try:
124
+ result = await task
125
+ except: # noqa
126
+ log.exception()
127
+ else:
128
+ self.results.append(result)
129
+ finally:
130
+ self.task_done()
@@ -7,16 +7,11 @@ Generic pretty display utils.
7
7
  force it with :envvar:`FORCE_COLOR`, ie. gitlab-ci etc
8
8
  """
9
9
  import difflib
10
+ import json
10
11
  import os
11
12
  import sys
12
13
  import yaml
13
14
 
14
- try:
15
- import jsonlight as json
16
- except ImportError:
17
- import json
18
-
19
-
20
15
  _print = print
21
16
 
22
17
 
@@ -109,8 +109,10 @@ class Mask:
109
109
  data[key] = self._mask(value)
110
110
  elif isinstance(data, list):
111
111
  return [self._mask(item) for item in data]
112
+ elif isinstance(data, set):
113
+ return {self._mask(item) for item in data}
112
114
  elif isinstance(data, str):
113
- for value in self.values:
115
+ for value in sorted(self.values, key=len, reverse=True):
114
116
  data = data.replace(str(value), '***MASKED***')
115
117
  return data
116
118
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.3.4
3
+ Version: 5.0.0
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -13,9 +13,10 @@ Requires-Dist: docstring_parser
13
13
  Requires-Dist: pyyaml
14
14
  Requires-Dist: pygments
15
15
  Requires-Dist: structlog
16
- Provides-Extra: client
17
- Requires-Dist: httpx; extra == "client"
18
- Requires-Dist: truststore; extra == "client"
16
+ Provides-Extra: httpx
17
+ Requires-Dist: chttpx; extra == "httpx"
18
+ Provides-Extra: ansible
19
+ Requires-Dist: cansible; extra == "ansible"
19
20
  Provides-Extra: test
20
21
  Requires-Dist: freezegun; extra == "test"
21
22
  Requires-Dist: pytest; extra == "test"
@@ -6,7 +6,6 @@ cli2/__init__.py
6
6
  cli2/asyncio.py
7
7
  cli2/cli.py
8
8
  cli2/cli2.py
9
- cli2/client.py
10
9
  cli2/colors.py
11
10
  cli2/configuration.py
12
11
  cli2/decorators.py
@@ -24,12 +23,7 @@ cli2.egg-info/dependency_links.txt
24
23
  cli2.egg-info/entry_points.txt
25
24
  cli2.egg-info/requires.txt
26
25
  cli2.egg-info/top_level.txt
27
- cli2/ansible/__init__.py
28
- cli2/ansible/action.py
29
- cli2/ansible/playbook.py
30
- cli2/ansible/variables.py
31
26
  cli2/examples/__init__.py
32
- cli2/examples/client.py
33
27
  cli2/examples/conf.py
34
28
  cli2/examples/example.py
35
29
  cli2/examples/example_obj.py
@@ -3,9 +3,11 @@ pyyaml
3
3
  pygments
4
4
  structlog
5
5
 
6
- [client]
7
- httpx
8
- truststore
6
+ [ansible]
7
+ cansible
8
+
9
+ [httpx]
10
+ chttpx
9
11
 
10
12
  [test]
11
13
  freezegun
@@ -44,7 +44,7 @@ from setuptools import setup
44
44
 
45
45
  setup(
46
46
  name='cli2',
47
- version='4.3.4',
47
+ version='5.0.0',
48
48
  setup_requires='setupmeta',
49
49
  packages=['cli2'],
50
50
  install_requires=[
@@ -54,10 +54,8 @@ setup(
54
54
  'structlog',
55
55
  ],
56
56
  extras_require=dict(
57
- client=[
58
- 'httpx',
59
- 'truststore',
60
- ],
57
+ httpx=['chttpx'],
58
+ ansible=['cansible'],
61
59
  test=[
62
60
  'freezegun',
63
61
  'pytest',
@@ -1,24 +1,23 @@
1
1
  import cli2
2
+ import chttpx
2
3
  import httpx
3
4
  import mock
4
5
  import pytest
5
6
  import textwrap
6
7
  import yaml
7
8
 
8
- from cli2 import ansible
9
-
10
-
11
- class ActionModule(ansible.ActionBase):
12
- mask_keys = ['a']
13
-
14
- async def run_async(self):
15
- self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
9
+ import cansible
16
10
 
17
11
 
18
12
  @pytest.mark.asyncio
19
13
  async def test_mask(monkeypatch):
20
- printer = mock.Mock()
21
- monkeypatch.setattr(ActionModule, 'print', printer)
14
+ class ActionModule(cansible.ActionBase):
15
+ masked_keys = ['a']
16
+ print = mock.Mock()
17
+
18
+ async def run_async(self):
19
+ self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
20
+
22
21
  module = await ActionModule.run_test_async(facts=dict(mask_keys=['b']))
23
22
  # result is untouched
24
23
  assert module.result == {'x':
@@ -26,15 +25,27 @@ async def test_mask(monkeypatch):
26
25
  }
27
26
  # output has proper masking
28
27
  expected = "\x1b[94mx\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94ma\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94mb\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94mc\x1b[39;49;00m:\x1b[37m \x1b[39;49;00mc\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94md\x1b[39;49;00m:\x1b[37m \x1b[39;49;00mfoo ***MASKED*** rrr\x1b[37m\x1b[39;49;00m\n" # noqa
29
- printer.assert_called_once_with(expected)
28
+ module.print.assert_called_once_with(expected, mask=False)
29
+
30
+
31
+ def test_subprocess_remote(playbook):
32
+ playbook.task_add(
33
+ 'yourlabs.test.password_get',
34
+ no_log=True,
35
+ )
36
+ result = playbook()
37
+ expected = "foo:***MASKED***:bar\n\nansible_facts:\n\n mask_values:\n\n - \'***MASKED***\'\n\nsecret: \'***MASKED***\'\n\nstdout: foo:***MASKED***:bar" # noqa
38
+ assert expected in result['stdout']
39
+
40
+
30
41
 
31
42
 
32
43
  @pytest.mark.asyncio
33
44
  async def test_response_error(httpx_mock):
34
- class Client(cli2.Client):
45
+ class Client(chttpx.Client):
35
46
  mask_keys = ['secret']
36
47
 
37
- class Action(ansible.ActionBase):
48
+ class Action(cansible.ActionBase):
38
49
  async def client_factory(self):
39
50
  return Client(base_url='http://foo')
40
51
 
@@ -59,9 +70,9 @@ async def test_response_error(httpx_mock):
59
70
 
60
71
  @pytest.mark.asyncio
61
72
  async def test_option():
62
- class Action(ansible.ActionBase):
63
- fact = ansible.Option(fact='fact', default='default fact')
64
- arg = ansible.Option(arg='arg', fact='arg_fact')
73
+ class Action(cansible.ActionBase):
74
+ fact = cansible.Option(fact='fact', default='default fact')
75
+ arg = cansible.Option(arg='arg', fact='arg_fact')
65
76
 
66
77
  async def run_async(self):
67
78
  self.result['arg'] = self.arg
@@ -90,8 +101,8 @@ async def test_option():
90
101
  error="Missing arg `arg` or fact `arg_fact`",
91
102
  )
92
103
 
93
- class Action(ansible.ActionBase):
94
- name = ansible.Option('name')
104
+ class Action(cansible.ActionBase):
105
+ name = cansible.Option('name')
95
106
 
96
107
  async def run_async(self):
97
108
  self.result['name'] = self.name
@@ -108,8 +119,8 @@ async def test_diff(monkeypatch):
108
119
  _print = mock.Mock()
109
120
  monkeypatch.setattr(cli2.display, '_print', _print)
110
121
 
111
- class Action(ansible.ActionBase):
112
- name = ansible.Option('name', 'object_name', 'Test object')
122
+ class Action(cansible.ActionBase):
123
+ name = cansible.Option('name', 'object_name', 'Test object')
113
124
 
114
125
  async def run_async(self):
115
126
  self.before_set(dict(foo=1))
@@ -119,6 +130,41 @@ async def test_diff(monkeypatch):
119
130
  _print.assert_called_once_with(expected)
120
131
 
121
132
 
133
+ @pytest.mark.asyncio
134
+ async def test_fact_set():
135
+ class Action(cansible.ActionBase):
136
+ foo = cansible.Option(fact='foo')
137
+
138
+ async def run_async(self):
139
+ self.foo = 'bar'
140
+ assert self.foo == 'bar'
141
+
142
+ action = await Action.run_test_async(facts=dict(foo='bar'))
143
+ assert 'ansible_facts' not in action.result
144
+
145
+ action = await Action.run_test_async()
146
+ assert action.result['ansible_facts'] == dict(foo='bar')
147
+
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_fact_set_mutable():
151
+ class Action(cansible.ActionBase):
152
+ async def run_async(self):
153
+ assert self.mask_values is self.mask.values
154
+ self.mask_values.add('foo')
155
+
156
+ action = await Action.run_test_async()
157
+ assert 'mask_values' in action.facts_values
158
+ assert action.result['ansible_facts'] == dict(mask_values=['foo'])
159
+
160
+ action = await Action.run_test_async(facts=dict(mask_values=['foo']))
161
+ assert 'ansible_facts' not in action.result
162
+
163
+ action = await Action.run_test_async(facts=dict(mask_values=['bar']))
164
+ result = action.result['ansible_facts']['mask_values']
165
+ assert sorted(result) == ['bar', 'foo']
166
+
167
+
122
168
  def test_playbook_render(playbook):
123
169
  assert playbook.name == 'test_playbook_render'
124
170
  playbook.task_add(
@@ -165,3 +211,12 @@ def test_playbook_exec(playbook):
165
211
  result = playbook()
166
212
  assert result['changed'] == 0
167
213
  assert result['ok'] == 2
214
+
215
+
216
+ def test_masking_ansible_story(playbook):
217
+ with open('tests/test_ansible_masking.yml', 'r') as f:
218
+ data = yaml.safe_load(f.read())
219
+ playbook.tasks += data[0]['tasks']
220
+ result = playbook()
221
+ expected = "hello ***MASKED*** ***MASKED*** bye\n\nansible_facts:\n\n mask_values:\n\n - \'***MASKED***\'\n\n - \'***MASKED***\'\n\ncmd: echo hello ***MASKED*** ***MASKED*** bye\n\nstdout: hello ***MASKED*** ***MASKED*** bye" # noqa
222
+ assert expected in result['stdout']
@@ -1,10 +1,10 @@
1
- from cli2 import ansible
1
+ import cansible
2
2
  import pytest
3
3
  import os
4
4
 
5
5
 
6
6
  def test_story():
7
- variables = ansible.Variables(
7
+ variables = cansible.Variables(
8
8
  root_path=os.path.dirname(__file__),
9
9
  pass_path=os.path.dirname(__file__) + '/vault_pass',
10
10
  )
@@ -13,27 +13,34 @@ def test_story():
13
13
  assert variables['variables_vault.yml'] == dict(bar='foo')
14
14
 
15
15
 
16
- def test_exceptions():
17
- variables = ansible.Variables()
18
- with pytest.raises(Exception) as exc:
16
+ def test_unresolvable_path_error():
17
+ variables = cansible.Variables()
18
+ with pytest.raises(cansible.UnresolvablePathError) as exc:
19
19
  variables['variables.yml']
20
20
  assert exc.value.args == (
21
21
  'variables.yml must be absolute if root_path not set',
22
22
  )
23
23
 
24
- with pytest.raises(Exception) as exc:
25
- variables['/variables_vault.yml']
26
- assert exc.value.args == ('/variables_vault.yml does not exist',)
27
24
 
28
- variables = ansible.Variables(root_path=os.path.dirname(__file__))
29
- with pytest.raises(Exception) as exc:
25
+ def test_path_not_found_error():
26
+ variables = cansible.Variables()
27
+ with pytest.raises(cansible.PathNotFoundError) as exc:
28
+ variables['/nonexistent.yml']
29
+ assert exc.value.args == ('/nonexistent.yml does not exist',)
30
+
31
+
32
+ def test_vault_password_required_error():
33
+ variables = cansible.Variables(root_path=os.path.dirname(__file__))
34
+ with pytest.raises(cansible.VaultPasswordFileRequiredError) as exc:
30
35
  variables['variables_vault.yml']
31
36
  assert exc.value.args == ('Vault password required in pass_path',)
32
37
 
33
- variables = ansible.Variables(
38
+
39
+ def test_vault_password_file_not_found_error():
40
+ variables = cansible.Variables(
34
41
  root_path=os.path.dirname(__file__),
35
42
  pass_path='/does/not/exist',
36
43
  )
37
- with pytest.raises(Exception) as exc:
44
+ with pytest.raises(cansible.VaultPasswordFileNotFoundError) as exc:
38
45
  variables['variables_vault.yml']
39
46
  assert exc.value.args == ('/does/not/exist does not exist',)
@@ -0,0 +1,68 @@
1
+ import asyncio
2
+ import cli2
3
+ import pytest
4
+ from unittest import mock
5
+
6
+
7
+ def test_async_run_noloop():
8
+ async def task():
9
+ await asyncio.sleep(.01)
10
+
11
+ cli2.async_run(task())
12
+
13
+
14
+ @pytest.mark.asyncio
15
+ async def test_async_run_loop():
16
+ async def task():
17
+ await asyncio.sleep(.01)
18
+
19
+ cli2.async_run(task())
20
+
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_queue_basic():
24
+ async def task(i):
25
+ return i
26
+
27
+ queue = cli2.Queue()
28
+ await queue.run(task(1), task(2), task(3))
29
+
30
+ assert sorted(queue.results) == [1, 2, 3]
31
+
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_queue_concurrency():
35
+ start = asyncio.get_event_loop().time()
36
+
37
+ async def worker(i):
38
+ await asyncio.sleep(0.1)
39
+ return i
40
+
41
+ queue = cli2.Queue(num_workers=3)
42
+ await queue.run(worker(1), worker(2), worker(3))
43
+
44
+ duration = asyncio.get_event_loop().time() - start
45
+ assert duration < 0.15, 'Should run in parallel'
46
+ assert sorted(queue.results) == [1, 2, 3]
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_queue_error_handling(monkeypatch):
51
+ import cli2.asyncio
52
+ logger = mock.Mock()
53
+ monkeypatch.setattr(cli2.asyncio, 'log', logger)
54
+
55
+ async def worker(i):
56
+ if i == 2:
57
+ raise ValueError('test error')
58
+ await asyncio.sleep(0.01)
59
+ return i
60
+
61
+ queue = cli2.Queue()
62
+ await queue.run(worker(1), worker(2), worker(3))
63
+
64
+ # Error should not stop other tasks
65
+ assert queue.results == [1, 3]
66
+
67
+ # Exceptions should be printed though, not swallowed
68
+ logger.exception.assert_called_once()
@@ -47,7 +47,7 @@ def test_cli(name, command):
47
47
 
48
48
 
49
49
  def test_hide():
50
- autotest('tests/test_hide.txt', 'cli2-example-client object find --help')
50
+ autotest('tests/test_hide.txt', 'chttpx-example object find --help')
51
51
 
52
52
 
53
53
  def test_load_module():