stoobly-agent 1.10.1__py3-none-any.whl → 1.10.2__py3-none-any.whl

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 (155) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/__main__.py +10 -0
  3. stoobly_agent/app/cli/ca_cert_cli.py +9 -5
  4. stoobly_agent/app/cli/helpers/replay_facade.py +2 -2
  5. stoobly_agent/app/cli/intercept_cli.py +5 -5
  6. stoobly_agent/app/cli/request_cli.py +2 -2
  7. stoobly_agent/app/cli/scaffold/app.py +14 -5
  8. stoobly_agent/app/cli/scaffold/app_command.py +0 -4
  9. stoobly_agent/app/cli/scaffold/app_config.py +49 -2
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +145 -76
  11. stoobly_agent/app/cli/scaffold/constants.py +8 -1
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +3 -1
  13. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
  14. stoobly_agent/app/cli/scaffold/docker/service/builder.py +15 -49
  15. stoobly_agent/app/cli/scaffold/docker/service/configure_gateway.py +3 -0
  16. stoobly_agent/app/cli/scaffold/docker/template_files.py +112 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/build_decorator.py +1 -1
  18. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +31 -39
  19. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +1 -1
  20. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +1 -1
  21. stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py +2 -3
  22. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +1 -1
  23. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
  24. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +1 -1
  25. stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py +423 -0
  26. stoobly_agent/app/cli/scaffold/local/__init__.py +0 -0
  27. stoobly_agent/app/cli/scaffold/local/service/__init__.py +0 -0
  28. stoobly_agent/app/cli/scaffold/local/service/builder.py +72 -0
  29. stoobly_agent/app/cli/scaffold/local/workflow/__init__.py +0 -0
  30. stoobly_agent/app/cli/scaffold/local/workflow/builder.py +35 -0
  31. stoobly_agent/app/cli/scaffold/local/workflow/run_command.py +339 -0
  32. stoobly_agent/app/cli/scaffold/service_command.py +9 -1
  33. stoobly_agent/app/cli/scaffold/service_config.py +8 -0
  34. stoobly_agent/app/cli/scaffold/service_create_command.py +18 -6
  35. stoobly_agent/app/cli/scaffold/service_docker_compose.py +1 -1
  36. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +2 -2
  37. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/run +3 -0
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/run +3 -0
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/run +3 -0
  42. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.configure +5 -1
  43. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.init +5 -1
  44. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.run +14 -0
  45. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.configure +5 -1
  46. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.init +5 -1
  47. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.run +14 -0
  48. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.configure +5 -1
  49. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.init +5 -1
  50. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.run +14 -0
  51. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.configure +5 -1
  52. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.init +5 -1
  53. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.run +19 -0
  54. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.configure +5 -1
  55. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.init +5 -1
  56. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.run +19 -0
  57. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.configure +5 -1
  58. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.init +5 -1
  59. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.run +19 -0
  60. stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up +0 -1
  61. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.configure +5 -1
  62. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.init +5 -1
  63. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.run +14 -0
  64. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +5 -1
  65. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.init +5 -1
  66. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.run +14 -0
  67. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.configure +5 -1
  68. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.init +5 -1
  69. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.run +14 -0
  70. stoobly_agent/app/cli/scaffold/templates/constants.py +35 -19
  71. stoobly_agent/app/cli/scaffold/templates/factory.py +34 -18
  72. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run +21 -0
  73. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run +21 -0
  74. stoobly_agent/app/cli/scaffold/templates/workflow/mock/run +3 -0
  75. stoobly_agent/app/cli/scaffold/templates/workflow/record/run +3 -0
  76. stoobly_agent/app/cli/scaffold/templates/workflow/test/run +3 -0
  77. stoobly_agent/app/cli/scaffold/workflow_command.py +18 -4
  78. stoobly_agent/app/cli/scaffold/workflow_copy_command.py +5 -4
  79. stoobly_agent/app/cli/scaffold/workflow_create_command.py +31 -29
  80. stoobly_agent/app/cli/scaffold/workflow_run_command.py +18 -151
  81. stoobly_agent/app/cli/scaffold_cli.py +115 -161
  82. stoobly_agent/app/cli/scenario_cli.py +2 -2
  83. stoobly_agent/app/cli/types/test.py +2 -2
  84. stoobly_agent/app/cli/types/workflow_run_command.py +52 -3
  85. stoobly_agent/app/proxy/handle_mock_service.py +1 -1
  86. stoobly_agent/app/proxy/intercept_settings.py +5 -25
  87. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +177 -27
  88. stoobly_agent/app/proxy/mock/types/__init__.py +22 -1
  89. stoobly_agent/app/proxy/replay/body_parser_service.py +8 -5
  90. stoobly_agent/app/proxy/replay/multipart.py +15 -13
  91. stoobly_agent/app/proxy/replay/replay_request_service.py +2 -2
  92. stoobly_agent/app/proxy/run.py +3 -0
  93. stoobly_agent/app/proxy/test/context.py +0 -4
  94. stoobly_agent/app/proxy/test/context_abc.py +0 -5
  95. stoobly_agent/cli.py +61 -16
  96. stoobly_agent/config/data_dir.py +0 -8
  97. stoobly_agent/public/12-es2015.618ecfd5f735b801b50f.js +1 -0
  98. stoobly_agent/public/12-es5.618ecfd5f735b801b50f.js +1 -0
  99. stoobly_agent/public/index.html +1 -1
  100. stoobly_agent/public/runtime-es2015.77bcd31efed9e5d5d431.js +1 -0
  101. stoobly_agent/public/runtime-es5.77bcd31efed9e5d5d431.js +1 -0
  102. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +17 -6
  103. stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py +177 -0
  104. stoobly_agent/test/app/cli/scaffold/{cli_test.py → docker/cli_test.py} +1 -8
  105. stoobly_agent/test/app/cli/scaffold/{e2e_test.py → docker/e2e_test.py} +31 -16
  106. stoobly_agent/test/app/cli/scaffold/local/__init__.py +0 -0
  107. stoobly_agent/test/app/cli/scaffold/{cli_invoker.py → local/cli_invoker.py} +38 -32
  108. stoobly_agent/test/app/cli/scaffold/local/e2e_test.py +342 -0
  109. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  110. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +903 -2
  111. stoobly_agent/test/app/proxy/replay/body_parser_service_test.py +95 -3
  112. stoobly_agent/test/config/data_dir_test.py +2 -7
  113. stoobly_agent/test/test_helper.py +16 -5
  114. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/METADATA +4 -2
  115. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/RECORD +150 -122
  116. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/WHEEL +1 -1
  117. stoobly_agent/app/cli/helpers/shell.py +0 -26
  118. stoobly_agent/public/12-es2015.be58ed0ef449008b932e.js +0 -1
  119. stoobly_agent/public/12-es5.be58ed0ef449008b932e.js +0 -1
  120. stoobly_agent/public/runtime-es2015.f8c814b38b27708e91c1.js +0 -1
  121. stoobly_agent/public/runtime-es5.f8c814b38b27708e91c1.js +0 -1
  122. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  123. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/configure → configure} +0 -0
  124. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/init → init} +0 -0
  125. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  126. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/configure → configure} +0 -0
  127. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/init → init} +0 -0
  128. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  129. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/configure → configure} +0 -0
  130. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/init → init} +0 -0
  131. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  132. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/configure → configure} +0 -0
  133. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/init → init} +0 -0
  134. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  135. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/configure → configure} +0 -0
  136. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/init → init} +0 -0
  137. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  138. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/configure → configure} +0 -0
  139. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/init → init} +0 -0
  140. /stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  141. /stoobly_agent/app/cli/scaffold/templates/app/gateway/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  142. /stoobly_agent/app/cli/scaffold/templates/app/gateway/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  143. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/{.docker-compose.exec.yml → .docker-compose.yml} +0 -0
  144. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  145. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  146. /stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  147. /stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  148. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/configure → configure} +0 -0
  149. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/init → init} +0 -0
  150. /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/configure → configure} +0 -0
  151. /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/init → init} +0 -0
  152. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/configure → configure} +0 -0
  153. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/init → init} +0 -0
  154. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/entry_points.txt +0 -0
  155. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info/licenses}/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  import pdb
2
2
  import pytest
3
+ import base64
3
4
 
4
5
  from stoobly_agent.app.proxy.replay.body_parser_service import decode_response, encode_response
5
6
 
@@ -11,13 +12,36 @@ def multipart_string():
11
12
  def content_type():
12
13
  return "multipart/form-data; boundary=ce560532019a77d83195f9e9873e16a1"
13
14
 
15
+ @pytest.fixture
16
+ def image_data():
17
+ """A small 1x1 pixel PNG image in base64 format for testing."""
18
+ # This is a minimal 1x1 pixel PNG image
19
+ png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
20
+ return base64.b64decode(png_base64)
21
+
22
+ @pytest.fixture
23
+ def multipart_with_image_string(image_data):
24
+ """Multipart form data containing both text fields and an image file."""
25
+ boundary = "ce560532019a77d83195f9e9873e16a1"
26
+
27
+ # Create multipart form data with text field and image
28
+ result = f'--{boundary}\r\nContent-Disposition: form-data; name="title"\r\n\r\nTest Image Upload\r\n'.encode('utf-8')
29
+ result += f'--{boundary}\r\nContent-Disposition: form-data; name="image"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n'.encode('utf-8')
30
+ result += image_data
31
+ result += f'\r\n--{boundary}--\r\n'.encode('utf-8')
32
+
33
+ with open('/tmp/multi', 'wb') as fp:
34
+ fp.write(result)
35
+
36
+ return result
37
+
14
38
  class TestMultipart():
15
39
 
16
40
  def test_decodes_response(self, multipart_string: bytes, content_type: str):
17
41
  multidict = decode_response(multipart_string, content_type)
18
42
 
19
- assert multidict.get('author') == 'John Smith'
20
- assert multidict.get('file') == 'Hello World'
43
+ assert multidict.get('author') == b'John Smith'
44
+ assert multidict.get('file') == b'Hello World'
21
45
 
22
46
  def test_encodes_response(self, content_type: str):
23
47
  expected_params = { 'author': 'John Smith', 'file': 'Hello World'}
@@ -25,4 +49,72 @@ class TestMultipart():
25
49
 
26
50
  multidict = decode_response(multipart_string, content_type)
27
51
  for key in expected_params:
28
- assert multidict.get(key) == expected_params[key]
52
+ assert multidict.get(key) == expected_params[key].encode('utf-8')
53
+
54
+ def test_decodes_multipart_with_image(self, multipart_with_image_string: bytes, content_type: str, image_data: bytes):
55
+ """Test decoding multipart form data that contains both text fields and binary image data."""
56
+ multidict = decode_response(multipart_with_image_string, content_type)
57
+
58
+ # Check text field
59
+ assert multidict.get('title') == b'Test Image Upload'
60
+
61
+ # Check image field - should contain the binary image data
62
+ image_field = multidict.get('image')
63
+ assert image_field is not None
64
+ assert isinstance(image_field, bytes)
65
+ assert image_field == image_data
66
+ assert len(image_field) == len(image_data)
67
+
68
+ def test_encodes_multipart_with_image(self, content_type: str, image_data: bytes):
69
+ """Test encoding multipart form data that contains both text fields and binary image data."""
70
+ expected_params = {
71
+ 'title': 'Test Image Upload',
72
+ 'image': image_data
73
+ }
74
+
75
+ multipart_string = encode_response(expected_params, content_type)
76
+ multidict = decode_response(multipart_string, content_type)
77
+
78
+ # Verify text field
79
+ assert multidict.get('title') == b'Test Image Upload'
80
+
81
+ # Verify image field
82
+ decoded_image = multidict.get('image')
83
+ assert decoded_image is not None
84
+ assert isinstance(decoded_image, bytes)
85
+ assert decoded_image == image_data
86
+
87
+ def test_decode_error_handling(self):
88
+ """Test decode function error handling with malformed multipart data."""
89
+ from stoobly_agent.app.proxy.replay.multipart import decode
90
+
91
+ # Test with invalid content type
92
+ headers = {'content-type': 'invalid/type'}
93
+ result = decode(headers, b'some content')
94
+ assert result == b'some content' # Should return original content
95
+
96
+ # Test with missing boundary
97
+ headers = {'content-type': 'multipart/form-data'}
98
+ result = decode(headers, b'some content')
99
+ assert result == b'some content' # Should return original content
100
+
101
+ # Test with malformed multipart data
102
+ headers = {'content-type': 'multipart/form-data; boundary=test123'}
103
+ malformed_content = b'--test123\r\nincomplete part'
104
+ result = decode(headers, malformed_content)
105
+ # Should return original content or handle gracefully
106
+ assert result is not None # Should not crash
107
+
108
+ def test_decode_return_type_consistency(self):
109
+ """Test that decode function returns consistent types."""
110
+ from stoobly_agent.app.proxy.replay.multipart import decode
111
+
112
+ # Test with invalid input types
113
+ headers = {'content-type': 'multipart/form-data; boundary=test123'}
114
+
115
+ # Should return original content for invalid input
116
+ result = decode(headers, None)
117
+ assert result is None
118
+
119
+ result = decode(headers, 123) # int instead of bytes/str
120
+ assert result == 123
@@ -17,14 +17,9 @@ class TestDataDir():
17
17
  def original_cwd(self) -> str:
18
18
  return os.getcwd()
19
19
 
20
- @pytest.fixture(scope='class')
21
- def home_dir(self) -> str:
22
- return os.path.expanduser("~")
23
-
24
- def test_in_home(self, original_cwd: str, home_dir: str):
25
- # A previous test can put us in 'stoobly_agent/test/app/models/schemas/.stoobly'
20
+ def test_in_home(self, original_cwd: str):
26
21
  os.chdir(original_cwd)
27
- data_dir_path = os.path.join(home_dir, DATA_DIR_NAME)
22
+ data_dir_path = os.path.join(original_cwd, DATA_DIR_NAME)
28
23
  os.environ[ENV] = NONE
29
24
 
30
25
  result = DataDir.instance().path
@@ -1,29 +1,40 @@
1
1
  import os
2
2
  import pdb
3
+ import shutil
4
+ import tempfile
3
5
 
4
6
  from stoobly_agent import VERSION
5
7
  from stoobly_agent.config.constants.env_vars import ENV
6
8
  from stoobly_agent.app.settings.constants.mode import TEST
7
9
 
8
10
  from stoobly_agent.app.settings import Settings
9
- from stoobly_agent.config.data_dir import DataDir
11
+ from stoobly_agent.config.data_dir import DATA_DIR_NAME, DataDir
10
12
  from stoobly_agent.lib.orm import ORM
11
13
  from stoobly_agent.lib.orm.migrate_service import migrate
12
14
 
13
15
  DETERMINISTIC_GET_REQUEST_URL = 'https://dog.ceo/api/breeds/list/all'
14
16
  NON_DETERMINISTIC_GET_REQUEST_URL = 'https://www.google.com'
15
17
 
16
- def reset():
18
+ def reset(dir_name = 'stoobly-agent-test'):
17
19
  os.environ[ENV] = TEST
18
20
 
19
- DataDir.instance().remove() # Clean data dir for testing
21
+ # Create a temporary folder using tmpfile
22
+ temp_dir = os.path.join('/tmp', dir_name)
23
+ if os.path.exists(temp_dir):
24
+ shutil.rmtree(temp_dir)
25
+ os.makedirs(temp_dir)
26
+ os.chdir(temp_dir)
27
+
28
+ data_dir: DataDir = DataDir.instance()
29
+ data_dir.create()
20
30
 
21
31
  ORM.instance().initialize_db()
22
32
  migrate(VERSION)
23
33
 
24
- Settings.instance().reset_and_load()
34
+ settings: Settings = Settings.instance()
35
+ settings.reset_and_load()
25
36
 
26
- return Settings.instance()
37
+ return settings
27
38
 
28
39
  def assert_orm_request_equivalent(request_1, request_2):
29
40
  assert request_1.latency == request_2.latency
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: stoobly-agent
3
- Version: 1.10.1
3
+ Version: 1.10.2
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
+ License-File: LICENSE
6
7
  Author: Matt Le
7
8
  Author-email: themathewle@gmail.com
8
9
  Requires-Python: >=3.10,<4.0
@@ -12,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
15
17
  Requires-Dist: click (>=8.1.0,<9.0.0)
16
18
  Requires-Dist: diff-match-patch (>=v20241021,<20241022)
17
19
  Requires-Dist: distro (>=1.9.0,<1.10)