polyswarm-api 3.6.0__tar.gz → 3.7.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 (20) hide show
  1. {polyswarm-api-3.6.0/src/polyswarm_api.egg-info → polyswarm-api-3.7.0}/PKG-INFO +7 -3
  2. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/README.md +1 -1
  3. polyswarm-api-3.7.0/requirements.txt +7 -0
  4. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/setup.py +8 -6
  5. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api/__init__.py +1 -1
  6. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api/api.py +59 -18
  7. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api/core.py +24 -29
  8. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api/exceptions.py +1 -1
  9. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api/resources.py +149 -65
  10. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0/src/polyswarm_api.egg-info}/PKG-INFO +7 -3
  11. polyswarm-api-3.7.0/src/polyswarm_api.egg-info/requires.txt +2 -0
  12. polyswarm-api-3.6.0/requirements.txt +0 -7
  13. polyswarm-api-3.6.0/src/polyswarm_api.egg-info/requires.txt +0 -6
  14. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/LICENSE +0 -0
  15. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/MANIFEST.in +0 -0
  16. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/setup.cfg +0 -0
  17. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api/settings.py +0 -0
  18. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api.egg-info/SOURCES.txt +0 -0
  19. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api.egg-info/dependency_links.txt +0 -0
  20. {polyswarm-api-3.6.0 → polyswarm-api-3.7.0}/src/polyswarm_api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: polyswarm-api
3
- Version: 3.6.0
3
+ Version: 3.7.0
4
4
  Summary: Client library to simplify interacting with the PolySwarm consumer API
5
5
  Home-page: https://github.com/polyswarm/polyswarm-api
6
6
  Author: PolySwarm Developers
@@ -11,8 +11,12 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3.7
13
13
  Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
14
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
15
- Requires-Python: >3.5,<4
19
+ Requires-Python: >=3.7,<4
16
20
  Description-Content-Type: text/markdown
17
21
  License-File: LICENSE
18
22
 
@@ -21,7 +25,7 @@ License-File: LICENSE
21
25
  An interface to the PolySwarm Customer APIs.
22
26
  For an easy-to-use CLI tool, or as an example of how to use these APIs, please see [polyswarm-cli](https://github.com/polyswarm/polyswarm-cli)
23
27
 
24
- Supports Python 2.7 and greater.
28
+ Supports Python 3.7 and greater.
25
29
 
26
30
  ## Installation
27
31
 
@@ -3,7 +3,7 @@
3
3
  An interface to the PolySwarm Customer APIs.
4
4
  For an easy-to-use CLI tool, or as an example of how to use these APIs, please see [polyswarm-cli](https://github.com/polyswarm/polyswarm-cli)
5
5
 
6
- Supports Python 2.7 and greater.
6
+ Supports Python 3.7 and greater.
7
7
 
8
8
  ## Installation
9
9
 
@@ -0,0 +1,7 @@
1
+ requests>=2.31
2
+ pytest~=7.4
3
+ jsonschema~=3.0.2
4
+ responses==0.23.3
5
+ python-dateutil==2.9.0
6
+ vcrpy~=4.4; python_version == "3.7"
7
+ vcrpy~=5.0; python_version > "3.7"
@@ -9,7 +9,7 @@ with open('README.md', 'r') as readme:
9
9
 
10
10
  setup(
11
11
  name='polyswarm-api',
12
- version='3.6.0',
12
+ version='3.7.0',
13
13
  description='Client library to simplify interacting with the PolySwarm consumer API',
14
14
  long_description=long_description,
15
15
  long_description_content_type='text/markdown',
@@ -17,13 +17,11 @@ setup(
17
17
  author_email='info@polyswarm.io',
18
18
  url='https://github.com/polyswarm/polyswarm-api',
19
19
  license='MIT',
20
- python_requires='>3.5,<4',
20
+ python_requires='>=3.7,<4',
21
21
  install_requires=[
22
- 'requests~=2.22',
23
- 'future~=0.18',
24
- 'python-dateutil~=2.8',
22
+ 'requests~=2.31',
23
+ 'python-dateutil~=2.9',
25
24
  ],
26
- extras_require={':python_version < "3.0"': ['enum34==1.1.6']},
27
25
  include_package_data=True,
28
26
  packages=find_packages('src'),
29
27
  package_dir={'': 'src'},
@@ -33,6 +31,10 @@ setup(
33
31
  'Operating System :: OS Independent',
34
32
  'Programming Language :: Python :: 3.7',
35
33
  'Programming Language :: Python :: 3.8',
34
+ 'Programming Language :: Python :: 3.9',
35
+ 'Programming Language :: Python :: 3.10',
36
+ 'Programming Language :: Python :: 3.11',
37
+ 'Programming Language :: Python :: 3.12',
36
38
  'Programming Language :: Python :: Implementation :: PyPy',
37
39
  ]
38
40
  )
@@ -1,5 +1,5 @@
1
1
  # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names
2
- __version__ = '3.6.0'
2
+ __version__ = '3.7.0'
3
3
  __release_url__ = 'https://api.github.com/repos/polyswarm/polyswarm-api/releases/latest'
4
4
 
5
5
  from . import api
@@ -3,19 +3,14 @@ import time
3
3
 
4
4
  import polyswarm_api.core
5
5
 
6
- try:
7
- from urllib.parse import urlparse
8
- except ImportError:
9
- from urlparse import urlparse
10
-
11
- from future.utils import string_types
6
+ from urllib.parse import urlparse
12
7
 
13
8
  from polyswarm_api import exceptions, resources, settings
14
9
 
15
10
  logger = logging.getLogger(__name__)
16
11
 
17
12
 
18
- class PolyswarmAPI(object):
13
+ class PolyswarmAPI:
19
14
  """A synchronous interface to the public and private PolySwarm APIs."""
20
15
 
21
16
  def __init__(self, key, uri=None, community=None, timeout=None, verify=True, **kwargs):
@@ -35,6 +30,11 @@ class PolyswarmAPI(object):
35
30
  self.session = polyswarm_api.core.PolyswarmSession(key, retries=settings.DEFAULT_RETRIES, verify=verify, **kwargs)
36
31
  self._engines = None
37
32
 
33
+ def __repr__(self):
34
+ clsname = f'{self.__class__.__module__}.{self.__class__.__name__}'
35
+ attrs = f'uri={self.uri!r}, community={self.community!r}, timeout={self.timeout!r}'
36
+ return f'<{clsname}({attrs}) at 0x{id(self):x}>'
37
+
38
38
  @property
39
39
  def engines(self):
40
40
  if not self._engines:
@@ -44,11 +44,11 @@ class PolyswarmAPI(object):
44
44
 
45
45
  def refresh_engine_cache(self):
46
46
  """
47
- Rrefresh the cached engine listing
47
+ Refresh the cached engine listing
48
48
  """
49
49
  engines = list(resources.Engine.list(self).result())
50
50
  if not engines:
51
- raise exceptions.InvalidValueException("Recieved empty engines listing")
51
+ raise exceptions.InvalidValueException("Received empty engines listing")
52
52
  self._engines = engines
53
53
 
54
54
  def wait_for(self, scan, timeout=settings.DEFAULT_SCAN_TIMEOUT):
@@ -66,8 +66,8 @@ class PolyswarmAPI(object):
66
66
  if scan_result.failed or scan_result.window_closed:
67
67
  return scan_result
68
68
  elif -1 < timeout < time.time() - start:
69
- raise exceptions.TimeoutException('Timed out waiting for scan {} to finish. Please try again.'
70
- .format(scan))
69
+ raise exceptions.TimeoutException(
70
+ f'Timed out waiting for scan {scan} to finish. Please try again.')
71
71
  else:
72
72
  time.sleep(settings.POLL_FREQUENCY)
73
73
 
@@ -229,7 +229,7 @@ class PolyswarmAPI(object):
229
229
  if hasattr(artifact, 'read') and hasattr(artifact.read, '__call__'):
230
230
  artifact = resources.LocalArtifact.from_handle(self, artifact, artifact_name=artifact_name or '',
231
231
  artifact_type=artifact_type)
232
- elif isinstance(artifact, string_types):
232
+ elif isinstance(artifact, str):
233
233
  if artifact_type == resources.ArtifactType.FILE:
234
234
  artifact = resources.LocalArtifact.from_path(self, artifact, artifact_type=artifact_type,
235
235
  artifact_name=artifact_name)
@@ -284,7 +284,7 @@ class PolyswarmAPI(object):
284
284
  return resources.ArtifactInstance.rescan_id(self, scan, scan_config=scan_config).result()
285
285
 
286
286
  def _parse_rule(self, rule):
287
- if isinstance(rule, string_types):
287
+ if isinstance(rule, str):
288
288
  rule, rule_id = resources.YaraRuleset(dict(yara=rule), api=self), None
289
289
  elif isinstance(rule, (resources.YaraRuleset, int)):
290
290
  rule, rule_id = None, rule
@@ -699,7 +699,7 @@ class PolyswarmAPI(object):
699
699
  if hasattr(artifact, 'read') and hasattr(artifact.read, '__call__'):
700
700
  artifact = resources.LocalArtifact.from_handle(self, artifact, artifact_name=artifact_name or '',
701
701
  artifact_type=artifact_type)
702
- elif isinstance(artifact, string_types):
702
+ elif isinstance(artifact, str):
703
703
  if artifact_type == resources.ArtifactType.FILE:
704
704
  artifact = resources.LocalArtifact.from_path(self, artifact, artifact_type=artifact_type,
705
705
  artifact_name=artifact_name)
@@ -823,7 +823,48 @@ class PolyswarmAPI(object):
823
823
  logger.info('List events')
824
824
  return resources.Events.list(self, **kwargs).result()
825
825
 
826
- def __repr__(self):
827
- clsname = '{0.__module__}.{0.__name__}'.format(self.__class__)
828
- attrs = 'uri={0.uri!r}, community={0.community!r}, timeout={0.timeout!r}'.format(self)
829
- return '<{}({}) at 0x{:x}>'.format(clsname, attrs, id(self))
826
+ def report_create(self, **kwargs):
827
+ return resources.ReportTask.create(self, **kwargs).result()
828
+
829
+ def report_get(self, **kwargs):
830
+ return resources.ReportTask.get(self, **kwargs).result()
831
+
832
+ def report_download(self, report_id, folder):
833
+ report = resources.ReportTask.get(self, id=report_id).result()
834
+ result = report.download_report(folder=folder).result()
835
+ result.handle.close()
836
+ return result
837
+
838
+ def report_template_logo_download(self, template_id, folder):
839
+ report = resources.ReportTemplate.get(self, id=template_id).result()
840
+ result = report.download_logo(folder).result()
841
+ result.handle.close()
842
+ return result
843
+
844
+ def report_template_logo_delete(self, template_id):
845
+ report = resources.ReportTemplate.get(self, id=template_id).result()
846
+ result = report.delete_logo().result()
847
+ return result
848
+
849
+ def report_template_logo_upload(self, template_id, logo_file, content_tpe):
850
+ report = resources.ReportTemplate.get(self, id=template_id).result()
851
+ result = report.upload_logo(logo_file, content_tpe).result()
852
+ return result
853
+
854
+ def report_template_create(self, **kwargs):
855
+ return resources.ReportTemplate.create(self, **kwargs).result()
856
+
857
+ def report_template_update(self, template_id, **kwargs):
858
+ return resources.ReportTemplate.update(self, id=template_id, **kwargs).result()
859
+
860
+ def report_template_get(self, template_id):
861
+ return resources.ReportTemplate.get(self, id=template_id).result()
862
+
863
+ def report_template_delete(self, template_id):
864
+ return resources.ReportTemplate.delete(self, id=template_id).result()
865
+
866
+ def report_template_list(self, is_default=None):
867
+ params = {}
868
+ if is_default is not None:
869
+ params['is_default'] = is_default
870
+ return resources.ReportTemplate.list(self, **params).result()
@@ -3,15 +3,11 @@ import logging
3
3
  from copy import deepcopy
4
4
  from urllib3 import Retry
5
5
  from binascii import unhexlify
6
- try:
7
- from json.decoder import JSONDecodeError
8
- except ImportError:
9
- JSONDecodeError = ValueError
6
+ from json.decoder import JSONDecodeError
10
7
 
11
8
  import requests
12
9
  import datetime as dt
13
10
  from dateutil import parser
14
- from future.utils import raise_from
15
11
  from requests.adapters import HTTPAdapter
16
12
  from urllib3.exceptions import InsecureRequestWarning
17
13
 
@@ -22,7 +18,7 @@ logger = logging.getLogger(__name__)
22
18
 
23
19
  class PolyswarmSession(requests.Session):
24
20
  def __init__(self, key, retries, user_agent=settings.DEFAULT_USER_AGENT, verify=True, **kwargs):
25
- super(PolyswarmSession, self).__init__(**kwargs)
21
+ super().__init__(**kwargs)
26
22
  logger.debug('Creating PolyswarmHTTP instance')
27
23
  self.requests_retry_session(retries=retries)
28
24
 
@@ -118,13 +114,12 @@ class PolyswarmRequest(object):
118
114
 
119
115
  def _bad_status_message(self):
120
116
  request_parameters = json.dumps(self.request_parameters, indent=4, sort_keys=True, cls=RequestParamsEncoder)
121
- message = "Error when running the request:\n{}\n" \
122
- "Return code: {}\n" \
123
- "Message: {}".format(request_parameters,
124
- self.status_code,
125
- self._result)
117
+ message = f"Error when running the request:\n{request_parameters}\n" \
118
+ f"Return code: {self.status_code}\n" \
119
+ f"Message: {self._result}"
126
120
  if self.errors:
127
- message = '{}\nErrors:\n{}'.format(message, '\n'.join(str(error) for error in self.errors))
121
+ errors = '\n'.join(str(error) for error in self.errors)
122
+ message = f'{message}\nErrors:\n{errors}'
128
123
  return message
129
124
 
130
125
  def _extract_json_body(self, result):
@@ -146,10 +141,10 @@ class PolyswarmRequest(object):
146
141
  if self.status_code // 100 != 2:
147
142
  self._extract_json_body(result)
148
143
  if self.status_code == 429:
149
- message = '{} This may mean you need to purchase a ' \
144
+ message = f'{self._result} This may mean you need to purchase a ' \
150
145
  'larger package, or that you have exceeded ' \
151
146
  'rate limits. If you continue to have issues, ' \
152
- 'please contact us at info@polyswarm.io.'.format(self._result)
147
+ 'please contact us at info@polyswarm.io.'
153
148
  raise exceptions.UsageLimitsExceededException(self, message)
154
149
  elif self.status_code == 404:
155
150
  raise exceptions.NotFoundException(self, self._result)
@@ -185,10 +180,10 @@ class PolyswarmRequest(object):
185
180
  self._result = self.result_parser.parse_result(self.api_instance, result, **self.parser_kwargs)
186
181
  except JSONDecodeError as e:
187
182
  if self.status_code == 404:
188
- raise raise_from(exceptions.NotFoundException(self, 'The requested endpoint does not exist.'), e)
183
+ raise exceptions.NotFoundException(self, 'The requested endpoint does not exist.') from e
189
184
  else:
190
- err_msg = 'Server returned non-JSON response [{}]: {}'.format(self.status_code, result)
191
- raise raise_from(exceptions.RequestException(self, err_msg), e)
185
+ err_msg = f'Server returned non-JSON response [{self.status_code}]: {result}'
186
+ raise exceptions.RequestException(self, err_msg) from e
192
187
 
193
188
  def __iter__(self):
194
189
  return self.consume_results()
@@ -232,12 +227,12 @@ class PolyswarmRequest(object):
232
227
  ).execute()
233
228
 
234
229
 
235
- class BaseResource(object):
230
+ class BaseResource:
236
231
  def __init__(self, content, *args, **kwargs):
237
232
  # hack to behave as in python 3, signature should be
238
233
  # __init__(self, content, *args, api=None, **kwargs)
239
234
  api = kwargs.pop('api', None)
240
- super(BaseResource, self).__init__(*args, **kwargs)
235
+ super().__init__(*args, **kwargs)
241
236
  self.api = api
242
237
  self._content = content
243
238
 
@@ -252,13 +247,13 @@ class BaseJsonResource(BaseResource):
252
247
  RESOURCE_ID_KEYS = ['id']
253
248
 
254
249
  def __init__(self, content, *args, **kwargs):
255
- super(BaseJsonResource, self).__init__(content, *args, **kwargs)
250
+ super().__init__(content, *args, **kwargs)
256
251
  self.json = content
257
252
 
258
253
  def __int__(self):
259
254
  id_ = getattr(self, 'id', None)
260
255
  if id_ is None:
261
- raise TypeError('Resource {} does not have an id and can not be cast to int'.format(type(self).__name__))
256
+ raise TypeError(f'Resource {type(self).__name__} does not have an id and can not be cast to int')
262
257
  return int(id_)
263
258
 
264
259
  def _get(self, path, default=None, content=None):
@@ -271,16 +266,16 @@ class BaseJsonResource(BaseResource):
271
266
  try:
272
267
  for attribute in path.split('.'):
273
268
  if obj is None:
274
- raise KeyError('{} is None, can not resolve full path'.format(previous_attribute))
269
+ raise KeyError(f'{previous_attribute} is None, can not resolve full path')
275
270
  if attribute.endswith(']'):
276
271
  # handling the list case, e.g.: "root.list_attr[2]"
277
272
  attribute, _, index = attribute.rpartition('[')
278
273
  index = int(index.rstrip(']'))
279
274
  obj = obj[attribute]
280
275
  if obj is None:
281
- raise KeyError('{} is None, but is it supposed to be a list'.format(attribute))
276
+ raise KeyError(f'{attribute} is None, but is it supposed to be a list')
282
277
  elif not isinstance(obj, list):
283
- raise ValueError('Can not access index for {}, it is not a list.'.format(attribute))
278
+ raise ValueError(f'Can not access index for {attribute}, it is not a list.')
284
279
  else:
285
280
  obj = obj[index]
286
281
  else:
@@ -469,7 +464,7 @@ def is_valid_sha256(value):
469
464
  return is_hex(value)
470
465
 
471
466
 
472
- class Hashable(object):
467
+ class Hashable:
473
468
  SUPPORTED_HASH_TYPES = {
474
469
  'sha1': is_valid_sha1,
475
470
  'sha256': is_valid_sha256,
@@ -482,7 +477,7 @@ class Hashable(object):
482
477
  hash_value = kwargs.pop('hash_value', None)
483
478
  hash_type = kwargs.pop('hash_type', None)
484
479
  validate_hash = kwargs.pop('validate_hash', False)
485
- super(Hashable, self).__init__(*args, **kwargs)
480
+ super().__init__(*args, **kwargs)
486
481
 
487
482
  self._hash = hash_value.strip() if hash_value is not None else None
488
483
 
@@ -494,7 +489,7 @@ class Hashable(object):
494
489
  self._hash_type = self.resolve_hash_type()
495
490
 
496
491
  if self._hash_type is None:
497
- raise exceptions.InvalidValueException('Invalid hash provided: {}'.format(self._hash))
492
+ raise exceptions.InvalidValueException(f'Invalid hash provided: {self._hash}')
498
493
 
499
494
  if validate_hash:
500
495
  self.validate()
@@ -514,8 +509,8 @@ class Hashable(object):
514
509
  def validate(self):
515
510
  hash_type = self.resolve_hash_type()
516
511
  if self.hash_type != hash_type:
517
- raise exceptions.InvalidValueException('Detected hash type {}, got type {} for hash {}'
518
- .format(hash_type, self.hash_type, self.hash))
512
+ raise exceptions.InvalidValueException(
513
+ f'Detected hash type {hash_type}, got type {self.hash_type} for hash {self.hash}')
519
514
 
520
515
  def resolve_hash_type(self):
521
516
  for hash_type, validator in self.SUPPORTED_HASH_TYPES.items():
@@ -20,7 +20,7 @@ class TimeoutException(PolyswarmAPIException):
20
20
 
21
21
  class RequestException(PolyswarmException):
22
22
  def __init__(self, request, *args):
23
- super(PolyswarmException, self).__init__(*args)
23
+ super().__init__(*args)
24
24
  self.request = request
25
25
 
26
26
 
@@ -5,15 +5,9 @@ import functools
5
5
  import requests
6
6
  from enum import Enum
7
7
  from hashlib import sha256 as _sha256, sha1 as _sha1, md5 as _md5
8
+ from urllib.parse import urlparse
8
9
 
9
- try:
10
- from urllib.parse import urlparse
11
- except ImportError:
12
- from urlparse import urlparse
13
-
14
- from future.utils import raise_from, string_types
15
-
16
- # Windows might rase an OSError instead of an ImportError like this
10
+ # Windows might raise an OSError instead of an ImportError like this
17
11
  # OSError: [WinError 193] %1 is not a valid Win32 application
18
12
  try:
19
13
  import yara
@@ -33,7 +27,7 @@ class Engine(core.BaseJsonResource):
33
27
  RESOURCE_ENDPOINT = '/microengines'
34
28
 
35
29
  def __init__(self, content, api=None):
36
- super(Engine, self).__init__(content=content, api=api)
30
+ super().__init__(content=content, api=api)
37
31
  self.id = str(content['id'])
38
32
  self.name = content['name']
39
33
 
@@ -95,7 +89,7 @@ class Metadata(core.BaseJsonResource):
95
89
  KNOWN_KEYS = {'artifact', 'exiftool', 'hash', 'lief', 'pefile', 'scan', 'strings'}
96
90
 
97
91
  def __init__(self, content, api=None):
98
- super(Metadata, self).__init__(content=content, api=api)
92
+ super().__init__(content=content, api=api)
99
93
  self.created = core.parse_isoformat(self.artifact.get('created'))
100
94
 
101
95
  self.id = self._get('artifact.id')
@@ -144,7 +138,7 @@ class Metadata(core.BaseJsonResource):
144
138
  params.extend(('ips', v) for v in ips)
145
139
  params.extend(('urls', v) for v in urls)
146
140
  params.extend(('domains', v) for v in domains)
147
- super_params, json_params = super(Metadata, cls)._get_params(**kwargs)
141
+ super_params, json_params = super()._get_params(**kwargs)
148
142
  params.extend(super_params.items())
149
143
  return params, json_params
150
144
 
@@ -159,7 +153,7 @@ class IOC(core.BaseJsonResource):
159
153
  api,
160
154
  {
161
155
  'method': 'GET',
162
- 'url': '{}/{}/{}/{}'.format(api.uri, path, hash_type, hash_value),
156
+ 'url': f'{api.uri}/{path}/{hash_type}/{hash_value}',
163
157
  'params': {
164
158
  'hide_known_good': hide_known_good,
165
159
  'community': api.community,
@@ -183,7 +177,7 @@ class IOC(core.BaseJsonResource):
183
177
  api,
184
178
  {
185
179
  'method': 'GET',
186
- 'url': '{}/ioc/search'.format(api.uri),
180
+ 'url': f'{api.uri}/ioc/search',
187
181
  'params': params
188
182
  },
189
183
  result_parser=cls,
@@ -195,7 +189,7 @@ class IOC(core.BaseJsonResource):
195
189
  api,
196
190
  {
197
191
  'method': 'GET',
198
- 'url': '{}/ioc/known'.format(api.uri),
192
+ 'url': f'{api.uri}/ioc/known',
199
193
  'params': {
200
194
  'ip': ips,
201
195
  'domain': domains
@@ -210,7 +204,7 @@ class IOC(core.BaseJsonResource):
210
204
  api,
211
205
  {
212
206
  'method': 'POST',
213
- 'url': '{}/ioc/known'.format(api.uri),
207
+ 'url': f'{api.uri}/ioc/known',
214
208
  'json': {
215
209
  'type': type,
216
210
  'host': host,
@@ -227,7 +221,7 @@ class IOC(core.BaseJsonResource):
227
221
  api,
228
222
  {
229
223
  'method': 'POST',
230
- 'url': '{}/ioc/known'.format(api.uri),
224
+ 'url': f'{api.uri}/ioc/known',
231
225
  'json': {
232
226
  'type': type,
233
227
  'host': host,
@@ -244,7 +238,7 @@ class IOC(core.BaseJsonResource):
244
238
  api,
245
239
  {
246
240
  'method': 'PUT',
247
- 'url': '{}/ioc/known'.format(api.uri),
241
+ 'url': f'{api.uri}/ioc/known',
248
242
  'json': {
249
243
  'id': id,
250
244
  'type': type,
@@ -262,7 +256,7 @@ class IOC(core.BaseJsonResource):
262
256
  api,
263
257
  {
264
258
  'method': 'DELETE',
265
- 'url': '{}/ioc/known'.format(api.uri),
259
+ 'url': f'{api.uri}/ioc/known',
266
260
  'params': {
267
261
  'id': id
268
262
  }
@@ -275,8 +269,8 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
275
269
  RESOURCE_ENDPOINT = '/instance'
276
270
 
277
271
  def __init__(self, content, api=None):
278
- super(ArtifactInstance, self).__init__(content=content, api=api,
279
- hash_value=content['sha256'], hash_type='sha256')
272
+ super().__init__(content=content, api=api,
273
+ hash_value=content['sha256'], hash_type='sha256')
280
274
  # Artifact fields
281
275
  self.sha256 = content['sha256']
282
276
  self.artifact_id = content.get('artifact_id')
@@ -342,7 +336,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
342
336
  api,
343
337
  {
344
338
  'method': 'HEAD',
345
- 'url': '{}/search/hash/{}'.format(api.uri, hash_type),
339
+ 'url': f'{api.uri}/search/hash/{hash_type}',
346
340
  'params': {
347
341
  'hash': hash_value,
348
342
  'community': api.community,
@@ -356,7 +350,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
356
350
  api,
357
351
  {
358
352
  'method': 'GET',
359
- 'url': '{}/search/hash/{}'.format(api.uri, hash_type),
353
+ 'url': f'{api.uri}/search/hash/{hash_type}',
360
354
  'params': {
361
355
  'hash': hash_value,
362
356
  'community': api.community,
@@ -371,7 +365,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
371
365
  api,
372
366
  {
373
367
  'method': 'GET',
374
- 'url': '{}/search/url'.format(api.uri),
368
+ 'url': f'{api.uri}/search/url',
375
369
  'params': {
376
370
  'url': url,
377
371
  'community': api.community,
@@ -386,7 +380,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
386
380
  api,
387
381
  {
388
382
  'method': 'GET',
389
- 'url': '{}/search/instances'.format(api.uri),
383
+ 'url': f'{api.uri}/search/instances',
390
384
  'params': {
391
385
  'hash': hash_value,
392
386
  'community': api.community,
@@ -399,7 +393,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
399
393
  def submit(cls, api, artifact, artifact_name, artifact_type, scan_config=None):
400
394
  parameters = {
401
395
  'method': 'POST',
402
- 'url': '{}/consumer/submission/{}'.format(api.uri, api.community),
396
+ 'url': f'{api.uri}/consumer/submission/{api.community}',
403
397
  'files': {
404
398
  'file': (artifact_name, artifact),
405
399
  },
@@ -420,8 +414,8 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
420
414
  def rescan(cls, api, hash_value, hash_type, scan_config=None):
421
415
  parameters = {
422
416
  'method': 'POST',
423
- 'url': '{}/consumer/submission/{}/rescan/{}/{}'.format(api.uri, api.community, hash_type, hash_value),
424
- 'data': { 'community': api.community }
417
+ 'url': f'{api.uri}/consumer/submission/{api.community}/rescan/{hash_type}/{hash_value}',
418
+ 'data': {'community': api.community}
425
419
  }
426
420
  if scan_config:
427
421
  parameters['data']['scan-config'] = scan_config
@@ -435,7 +429,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
435
429
  def rescan_id(cls, api, submission_id, scan_config=None):
436
430
  parameters = {
437
431
  'method': 'POST',
438
- 'url': '{}/consumer/submission/{}/rescan/{}'.format(api.uri, api.community, int(submission_id)),
432
+ 'url': f'{api.uri}/consumer/submission/{api.community}/rescan/{int(submission_id)}',
439
433
  }
440
434
  if scan_config:
441
435
  parameters.setdefault('data', {})['scan-config'] = scan_config
@@ -451,7 +445,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
451
445
  api,
452
446
  {
453
447
  'method': 'GET',
454
- 'url': '{}/consumer/submission/{}/{}'.format(api.uri, api.community, int(submission_id)),
448
+ 'url': f'{api.uri}/consumer/submission/{api.community}/{int(submission_id)}',
455
449
  },
456
450
  result_parser=cls,
457
451
  ).execute()
@@ -460,7 +454,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
460
454
  def metadata_rerun(cls, api, hashes, analyses=None, skip_es=None):
461
455
  parameters = {
462
456
  'method': 'POST',
463
- 'url': '{}/consumer/metadata'.format(api.uri),
457
+ 'url': f'{api.uri}/consumer/metadata',
464
458
  'json': {'hashes': hashes},
465
459
  }
466
460
  if analyses:
@@ -499,7 +493,7 @@ class ArtifactArchive(core.BaseJsonResource):
499
493
  RESOURCE_ENDPOINT = '/consumer/download/stream'
500
494
 
501
495
  def __init__(self, content, api=None):
502
- super(ArtifactArchive, self).__init__(content=content, api=api)
496
+ super().__init__(content=content, api=api)
503
497
  self.id = content['id']
504
498
  self.community = content['community']
505
499
  self.created = core.parse_isoformat(content['created'])
@@ -510,7 +504,7 @@ class AssertionsJob(core.BaseJsonResource):
510
504
  RESOURCE_ENDPOINT = '/consumer/assertions-job'
511
505
 
512
506
  def __init__(self, content, api=None):
513
- super(AssertionsJob, self).__init__(content=content, api=api)
507
+ super().__init__(content=content, api=api)
514
508
  self.id = content['id']
515
509
  self.engine_id = content['engine_id']
516
510
  self.created = core.parse_isoformat(content['created'])
@@ -530,7 +524,7 @@ class VotesJob(core.BaseJsonResource):
530
524
  RESOURCE_ENDPOINT = '/consumer/votes-job'
531
525
 
532
526
  def __init__(self, content, api=None):
533
- super(VotesJob, self).__init__(content=content, api=api)
527
+ super().__init__(content=content, api=api)
534
528
  self.id = content['id']
535
529
  self.engine_id = content['engine_id']
536
530
  self.created = core.parse_isoformat(content['created'])
@@ -581,7 +575,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
581
575
  raise exceptions.InvalidValueException('At least one of path or handle must be defined.')
582
576
 
583
577
  # initialize super classes and default values
584
- super(LocalArtifact, self).__init__(response, api=api, hash_type='sha256')
578
+ super().__init__(response, api=api, hash_type='sha256')
585
579
  self.sha256 = None
586
580
  self.sha1 = None
587
581
  self.md5 = None
@@ -614,13 +608,8 @@ class LocalArtifact(core.BaseResource, core.Hashable):
614
608
  remove_on_error = False
615
609
  try:
616
610
  if folder:
617
- # TODO: this should be replaced with os.makedirs(path, exist_ok=True)
618
- # once we drop support to python 2.7
619
611
  if not os.path.exists(folder):
620
- try:
621
- os.makedirs(folder)
622
- except FileExistsError:
623
- pass
612
+ os.makedirs(folder, exist_ok=True)
624
613
  remove_on_error = True
625
614
  self.handle = open(os.path.join(folder, self.artifact_name), mode='wb+', **kwargs)
626
615
  else:
@@ -652,7 +641,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
652
641
  api,
653
642
  {
654
643
  'method': 'GET',
655
- 'url': '{}/consumer/download/{}/{}'.format(api.uri, hash_type, hash_value),
644
+ 'url': f'{api.uri}/consumer/download/{hash_type}/{hash_value}',
656
645
  'stream': True,
657
646
  'params': { 'community': api.community },
658
647
  },
@@ -668,7 +657,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
668
657
  api,
669
658
  {
670
659
  'method': 'GET',
671
- 'url': '{}/instance/download'.format(api.uri),
660
+ 'url': f'{api.uri}/instance/download',
672
661
  'stream': True,
673
662
  'params': {'instance_id': instance_id},
674
663
  },
@@ -729,7 +718,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
729
718
 
730
719
  @classmethod
731
720
  def from_path(cls, api, path, artifact_type=None, analyze=False, artifact_name=None, **kwargs):
732
- if not isinstance(path, string_types):
721
+ if not isinstance(path, str):
733
722
  raise exceptions.InvalidValueException('Path should be a string')
734
723
  artifact_name = artifact_name or os.path.basename(path)
735
724
  handle = open(path, mode='rb', **kwargs)
@@ -739,7 +728,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
739
728
 
740
729
  @classmethod
741
730
  def from_content(cls, api, content, artifact_name=None, artifact_type=None, analyze=False):
742
- if isinstance(content, string_types):
731
+ if isinstance(content, str):
743
732
  content = content.encode("utf8")
744
733
  handle = io.BytesIO(content)
745
734
  # create the LocalHandle with the given handle and don't write anything to it
@@ -749,7 +738,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
749
738
  @property
750
739
  def hash(self):
751
740
  self.analyze_artifact()
752
- return super(LocalArtifact, self).hash
741
+ return super().hash
753
742
 
754
743
  def analyze_artifact(self, force=False):
755
744
  if not self.analyzed or force:
@@ -776,7 +765,7 @@ class YaraRuleset(core.BaseJsonResource):
776
765
  RESOURCE_ENDPOINT = '/hunt/rule'
777
766
 
778
767
  def __init__(self, content, api=None):
779
- super(YaraRuleset, self).__init__(content, api=api)
768
+ super().__init__(content, api=api)
780
769
  self.id = content.get('id')
781
770
  self.livescan_id = content.get('livescan_id')
782
771
  self.livescan_created = content.get('livescan_created')
@@ -796,7 +785,7 @@ class LiveHuntResult(core.BaseJsonResource):
796
785
  RESOURCE_ENDPOINT = '/hunt/live'
797
786
 
798
787
  def __init__(self, content, api=None):
799
- super(LiveHuntResult, self).__init__(content=content, api=api)
788
+ super().__init__(content=content, api=api)
800
789
  self.id = content['id']
801
790
  self.livescan_id = content['livescan_id']
802
791
  self.instance_id = content['instance_id']
@@ -820,7 +809,7 @@ class HistoricalHunt(core.BaseJsonResource):
820
809
  RESOURCE_ENDPOINT = '/hunt/historical'
821
810
 
822
811
  def __init__(self, content, api=None):
823
- super(HistoricalHunt, self).__init__(content=content, api=api)
812
+ super().__init__(content=content, api=api)
824
813
  # active only present for live hunts
825
814
  self.id = content['id']
826
815
  self.created = core.parse_isoformat(content['created'])
@@ -842,7 +831,7 @@ class HistoricalHuntResult(core.BaseJsonResource):
842
831
  RESOURCE_ENDPOINT = '/hunt/historical/results'
843
832
 
844
833
  def __init__(self, content, api=None):
845
- super(HistoricalHuntResult, self).__init__(content=content, api=api)
834
+ super().__init__(content=content, api=api)
846
835
  self.id = content['id']
847
836
  self.historicalscan_id = content['historicalscan_id']
848
837
  self.instance_id = content['instance_id']
@@ -856,6 +845,7 @@ class HistoricalHuntResult(core.BaseJsonResource):
856
845
  self.download_url = content.get('download_url')
857
846
  self.community = content.get('community')
858
847
 
848
+
859
849
  class HistoricalHuntResultList(HistoricalHuntResult):
860
850
  RESOURCE_ENDPOINT = '/hunt/historical/results/list'
861
851
 
@@ -865,7 +855,7 @@ class TagLink(core.BaseJsonResource):
865
855
  RESOURCE_ID_KEYS = ['hash']
866
856
 
867
857
  def __init__(self, content, api=None):
868
- super(TagLink, self).__init__(content, api=api)
858
+ super().__init__(content, api=api)
869
859
  self.id = content.get('id')
870
860
  self.sha256 = content.get('sha256')
871
861
  self.created = core.parse_isoformat(content.get('created'))
@@ -892,7 +882,7 @@ class MalwareFamily(core.BaseJsonResource):
892
882
  RESOURCE_ID_KEYS = ['name']
893
883
 
894
884
  def __init__(self, content, api=None):
895
- super(MalwareFamily, self).__init__(content, api=api)
885
+ super().__init__(content, api=api)
896
886
  self.id = content.get('id')
897
887
  self.created = core.parse_isoformat(content.get('created'))
898
888
  self.updated = core.parse_isoformat(content.get('updated'))
@@ -905,7 +895,7 @@ class Tag(core.BaseJsonResource):
905
895
  RESOURCE_ID_KEYS = ['name']
906
896
 
907
897
  def __init__(self, content, api=None):
908
- super(Tag, self).__init__(content, api=api)
898
+ super().__init__(content, api=api)
909
899
  self.id = content.get('id')
910
900
  self.created = core.parse_isoformat(content.get('created'))
911
901
  self.updated = core.parse_isoformat(content.get('updated'))
@@ -919,7 +909,7 @@ class Tag(core.BaseJsonResource):
919
909
 
920
910
  class Assertion(core.BaseJsonResource):
921
911
  def __init__(self, content, api=None, scanfile=None):
922
- super(Assertion, self).__init__(content, api=api)
912
+ super().__init__(content, api=api)
923
913
  self.scanfile = scanfile
924
914
  self.author = content['author']
925
915
  self.author_name = content['author_name']
@@ -936,7 +926,7 @@ class Assertion(core.BaseJsonResource):
936
926
 
937
927
  class Vote(core.BaseJsonResource):
938
928
  def __init__(self, content, api=None, scanfile=None):
939
- super(Vote, self).__init__(content, api=api)
929
+ super().__init__(content, api=api)
940
930
  self.scanfile = scanfile
941
931
  self.arbiter = content['arbiter']
942
932
  self.vote = content['vote']
@@ -961,9 +951,8 @@ class ArtifactType(Enum):
961
951
  try:
962
952
  return ArtifactType[value.upper()]
963
953
  except Exception as e:
964
- raise raise_from(
965
- exceptions.InvalidValueException(
966
- 'Unable to get the artifact type from the provided value {}'.format(value)), e)
954
+ raise exceptions.InvalidValueException(
955
+ f'Unable to get the artifact type from the provided value {value}') from e
967
956
 
968
957
  @staticmethod
969
958
  def to_string(artifact_type):
@@ -984,7 +973,7 @@ class ArtifactType(Enum):
984
973
 
985
974
  class Hash(core.Hashable):
986
975
  def __init__(self, hash_, hash_type=None, validate_hash=True):
987
- super(Hash, self).__init__(hash_value=hash_, hash_type=hash_type, validate_hash=validate_hash)
976
+ super().__init__(hash_value=hash_, hash_type=hash_type, validate_hash=validate_hash)
988
977
 
989
978
  @classmethod
990
979
  def from_hashable(cls, hash_, hash_type=None):
@@ -998,8 +987,8 @@ class Hash(core.Hashable):
998
987
  """
999
988
  if issubclass(type(hash_), core.Hashable):
1000
989
  if hash_type and hash_.hash_type != hash_type:
1001
- raise exceptions.InvalidValueException('Detected hash type {}, got {} for hashable {}'
1002
- .format(hash_.hash_type, hash_type, hash_.hash))
990
+ raise exceptions.InvalidValueException(
991
+ f'Detected hash type {hash_.hash_type}, got {hash_type} for hashable {hash_.hash}')
1003
992
  return Hash(hash_.hash, hash_type=hash_type)
1004
993
  return Hash(hash_, hash_type=hash_type)
1005
994
 
@@ -1010,14 +999,14 @@ class Hash(core.Hashable):
1010
999
  return self.hash
1011
1000
 
1012
1001
  def __repr__(self):
1013
- return "{}={}".format(self.hash_type, self.hash)
1002
+ return f"{self.hash_type}={self.hash}"
1014
1003
 
1015
1004
 
1016
1005
  class SandboxTask(core.BaseJsonResource):
1017
1006
  RESOURCE_ENDPOINT = '/sandbox/sandboxtask'
1018
1007
 
1019
1008
  def __init__(self, content, api=None):
1020
- super(SandboxTask, self).__init__(content, api=api)
1009
+ super().__init__(content, api=api)
1021
1010
  self.id = content['id']
1022
1011
  self.community = content['community']
1023
1012
  self.sandbox = content['sandbox']
@@ -1083,7 +1072,7 @@ class SandboxTask(core.BaseJsonResource):
1083
1072
 
1084
1073
  class SandboxArtifact(core.BaseJsonResource):
1085
1074
  def __init__(self, content, api=None):
1086
- super(SandboxArtifact, self).__init__(content, api=api)
1075
+ super().__init__(content, api=api)
1087
1076
  self.created = content['created']
1088
1077
  self.id = content['id']
1089
1078
  self.instance_id = content['instance_id']
@@ -1097,7 +1086,7 @@ class SandboxProvider(core.BaseJsonResource):
1097
1086
  RESOURCE_ENDPOINT = "/sandbox/provider"
1098
1087
 
1099
1088
  def __init__(self, content, api=None):
1100
- super(SandboxProvider, self).__init__(content, api=api)
1089
+ super().__init__(content, api=api)
1101
1090
  self.slug = content['slug']
1102
1091
  self.name = content['name']
1103
1092
  self.tool = content['tool']
@@ -1113,9 +1102,104 @@ class Events(core.BaseJsonResource):
1113
1102
  RESOURCE_ENDPOINT = "/activity"
1114
1103
 
1115
1104
  def __init__(self, content, api=None):
1116
- super(Events, self).__init__(content, api=api)
1105
+ super().__init__(content, api=api)
1117
1106
  self.event_timestamp = content['event_timestamp']
1118
1107
  self.event_type = content['event_type']
1119
1108
  self.source = content['source']
1120
1109
  self.team_account_id = content['team_account_id']
1121
1110
  self.user_account_id = content['user_account_id']
1111
+
1112
+
1113
+ class ReportTask(core.BaseJsonResource):
1114
+ RESOURCE_ENDPOINT = "/reports"
1115
+
1116
+ def __init__(self, content, api=None):
1117
+ super().__init__(content, api=api)
1118
+ self.id = content['id']
1119
+ self.type = content['type']
1120
+ self.format = content['format']
1121
+ self.state = content['state']
1122
+ self.community = content['community']
1123
+ self.created = content['created']
1124
+ self.template_id = content.get('template_id')
1125
+ self.template_metadata = content.get('template_metadata', {})
1126
+ self.sandbox_task_id = content.get('sandbox_task_id')
1127
+ self.instance_id = content.get('instance_id')
1128
+ self.url = content['url']
1129
+
1130
+ def download_report(self, folder=None):
1131
+ """ This method is special, in that it is simply for downloading from S3 """
1132
+ return core.PolyswarmRequest(
1133
+ self.api,
1134
+ {
1135
+ 'method': 'GET',
1136
+ 'url': self.url,
1137
+ 'stream': True,
1138
+ 'headers': {'Authorization': None}
1139
+ },
1140
+ result_parser=LocalArtifact,
1141
+ folder=folder,
1142
+ ).execute()
1143
+
1144
+
1145
+ class ReportTemplate(core.BaseJsonResource):
1146
+ RESOURCE_ENDPOINT = "/reports/templates"
1147
+
1148
+ def __init__(self, content, api=None):
1149
+ super().__init__(content, api=api)
1150
+ if content:
1151
+ self.id = content['id']
1152
+ self.created = content['created']
1153
+ self.template_name = content['template_name']
1154
+ self.includes = content.get('includes')
1155
+ self.primary_color = content.get('primary_color')
1156
+ self.footer_text = content.get('footer_text')
1157
+ self.last_page_text = content.get('last_page_text')
1158
+ self.is_default = content.get('is_default', False)
1159
+ self.logo_content_length = content.get('logo_content_length')
1160
+ self.logo_url = f"{self.api.uri}/reports/templates/logo?id={self.id}"
1161
+ self.logo_content_type = content.get('logo_content_type')
1162
+ self.logo_height = content.get('logo_height')
1163
+ self.logo_width = content.get('logo_width')
1164
+
1165
+ def download_logo(self, folder):
1166
+ return core.PolyswarmRequest(
1167
+ self.api,
1168
+ {
1169
+ 'method': 'GET',
1170
+ 'url': self.logo_url,
1171
+ },
1172
+ result_parser=LocalArtifact,
1173
+ folder=folder,
1174
+ ).execute()
1175
+
1176
+ def delete_logo(self):
1177
+ return core.PolyswarmRequest(
1178
+ self.api,
1179
+ {
1180
+ 'method': 'DELETE',
1181
+ 'url': self.logo_url,
1182
+ },
1183
+ ).execute()
1184
+
1185
+ def upload_logo(self, logo_file, content_tpe):
1186
+ if not logo_file:
1187
+ raise exceptions.InvalidValueException('A local file must be provided in order to upload')
1188
+ logo_file.seek(0, io.SEEK_END)
1189
+ length = logo_file.tell()
1190
+ logo_file.seek(0)
1191
+ if not length:
1192
+ raise exceptions.InvalidValueException('Empty file')
1193
+ # r = requests.put(self.upload_url, data=logo_file, **kwargs)
1194
+ # r.raise_for_status()
1195
+ # return r
1196
+ return core.PolyswarmRequest(
1197
+ self.api,
1198
+ {
1199
+ 'method': 'PUT',
1200
+ 'url': f'{self.api.uri}/reports/templates/logo?id={self.id}',
1201
+ 'data': logo_file,
1202
+ 'headers': {'Content-Type': content_tpe}
1203
+ },
1204
+ result_parser=self.__class__
1205
+ ).execute()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: polyswarm-api
3
- Version: 3.6.0
3
+ Version: 3.7.0
4
4
  Summary: Client library to simplify interacting with the PolySwarm consumer API
5
5
  Home-page: https://github.com/polyswarm/polyswarm-api
6
6
  Author: PolySwarm Developers
@@ -11,8 +11,12 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3.7
13
13
  Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
14
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
15
- Requires-Python: >3.5,<4
19
+ Requires-Python: >=3.7,<4
16
20
  Description-Content-Type: text/markdown
17
21
  License-File: LICENSE
18
22
 
@@ -21,7 +25,7 @@ License-File: LICENSE
21
25
  An interface to the PolySwarm Customer APIs.
22
26
  For an easy-to-use CLI tool, or as an example of how to use these APIs, please see [polyswarm-cli](https://github.com/polyswarm/polyswarm-cli)
23
27
 
24
- Supports Python 2.7 and greater.
28
+ Supports Python 3.7 and greater.
25
29
 
26
30
  ## Installation
27
31
 
@@ -0,0 +1,2 @@
1
+ requests~=2.31
2
+ python-dateutil~=2.9
@@ -1,7 +0,0 @@
1
- requests>=2.20
2
- pytest~=7.2
3
- jsonschema~=3.0.2
4
- responses==0.10.6
5
- python-dateutil==2.8.1
6
- future==0.18.2
7
- vcrpy~=5.0
@@ -1,6 +0,0 @@
1
- requests~=2.22
2
- future~=0.18
3
- python-dateutil~=2.8
4
-
5
- [:python_version < "3.0"]
6
- enum34==1.1.6
File without changes
File without changes
File without changes