squad 1.66__py3-none-any.whl → 1.67__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.

Potentially problematic release.


This version of squad might be problematic. Click here for more details.

squad/api/ci.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from django.http import HttpResponse, HttpResponseBadRequest
2
4
  from django.shortcuts import get_object_or_404
3
5
  from django.views.decorators.csrf import csrf_exempt
@@ -8,6 +10,7 @@ from squad.ci.exceptions import SubmissionIssue
8
10
  from squad.ci.tasks import submit, fetch
9
11
  from squad.ci.models import Backend, TestJob
10
12
  from squad.core.utils import log_addition
13
+ from squad.core.models import Project
11
14
 
12
15
 
13
16
  @require_http_methods(["POST"])
@@ -149,3 +152,42 @@ def resubmit_job(request, test_job_id, method='resubmit'):
149
152
  @csrf_exempt
150
153
  def force_resubmit_job(request, test_job_id):
151
154
  return resubmit_job(request, test_job_id, method='force_resubmit')
155
+
156
+
157
+ @require_http_methods(["POST"])
158
+ @csrf_exempt
159
+ def fetch_job(request, group_slug, project_slug, version, environment_slug, backend_name):
160
+ try:
161
+ backend = Backend.objects.get(name=backend_name)
162
+ except Backend.DoesNotExist:
163
+ return HttpResponseBadRequest("requested backend does not exist")
164
+
165
+ if not backend.supports_callbacks():
166
+ return HttpResponseBadRequest("requested backend does not support callbacks")
167
+
168
+ try:
169
+ project = Project.objects.get(slug=project_slug, group__slug=group_slug)
170
+ except Project.DoesNotExist:
171
+ return HttpResponseBadRequest("group/project does not exist")
172
+
173
+ try:
174
+ backend.validate_callback(request, project)
175
+ except Exception as e:
176
+ return HttpResponseBadRequest(f"request is not valid for this backend: {e}")
177
+
178
+ environment, _ = project.environments.get_or_create(slug=environment_slug)
179
+ build, _ = project.builds.get_or_create(version=version)
180
+
181
+ try:
182
+ payload = json.loads(request.body)
183
+ except Exception as e:
184
+ return HttpResponseBadRequest(f"payload failed to parse as json: {e}")
185
+
186
+ try:
187
+ test_job = backend.process_callback(payload, build, environment)
188
+ except Exception as e:
189
+ return HttpResponseBadRequest(f"malformed callback payload: {e}")
190
+
191
+ fetch.delay(test_job.id)
192
+
193
+ return HttpResponse(test_job.id, status=201)
squad/api/urls.py CHANGED
@@ -21,6 +21,7 @@ urlpatterns = [
21
21
  url(r'^submit/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern), views.add_test_run),
22
22
  url(r'^submitjob/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern), ci.submit_job),
23
23
  url(r'^watchjob/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern), ci.watch_job),
24
+ url(r'^fetchjob/(%s)/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern, slug_pattern), ci.fetch_job),
24
25
  url(r'^data/(%s)/(%s)' % (group_slug_pattern, slug_pattern), data.get),
25
26
  url(r'^resubmit/([0-9]+)', ci.resubmit_job),
26
27
  url(r'^forceresubmit/([0-9]+)', ci.force_resubmit_job),
squad/ci/backend/fake.py CHANGED
@@ -65,3 +65,6 @@ class Backend(object):
65
65
 
66
66
  def check_job_definition(self, definition):
67
67
  return True
68
+
69
+ def supports_callbacks(self):
70
+ return False
squad/ci/backend/null.py CHANGED
@@ -104,6 +104,25 @@ class Backend(object):
104
104
  """
105
105
  raise NotImplementedError
106
106
 
107
+ def supports_callbacks(self):
108
+ """
109
+ Returns True if this backend supports callbacks, False otherwise
110
+ """
111
+ return False
112
+
113
+ def validate_callback(self, request, project):
114
+ """
115
+ Raises an exception in case the request does not pass the validation
116
+ """
117
+ raise NotImplementedError
118
+
119
+ def process_callback(self, json_payload, build, environment, backend):
120
+ """
121
+ Returns a test_job if processing callback's payload fine, or raise exceptions
122
+ if something isn't right
123
+ """
124
+ raise NotImplementedError
125
+
107
126
  def format_message(self, msg):
108
127
  if self.data and hasattr(self.data, "name"):
109
128
  return self.data.name + ': ' + msg
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import hashlib
2
3
  import logging
3
4
  import re
@@ -8,8 +9,15 @@ import json
8
9
  from functools import reduce
9
10
  from urllib.parse import urljoin
10
11
 
12
+ from cryptography.hazmat.primitives.asymmetric import ec
13
+ from cryptography.hazmat.primitives import (
14
+ hashes,
15
+ serialization,
16
+ )
17
+
11
18
  from squad.ci.backend.null import Backend as BaseBackend
12
19
  from squad.ci.exceptions import FetchIssue, TemporaryFetchIssue
20
+ from squad.ci.models import TestJob
13
21
 
14
22
 
15
23
  logger = logging.getLogger('squad.ci.backend.tuxsuite')
@@ -83,16 +91,43 @@ class Backend(BaseBackend):
83
91
  # The regex below is supposed to find only one match
84
92
  return matches[0]
85
93
 
94
+ def generate_job_id(self, result_type, result):
95
+ """
96
+ The job id for TuxSuite results is generated using 3 pieces of info:
97
+ 1. If it's either "BUILD" or "TEST" result;
98
+ 2. The TuxSuite project. Ex: "linaro/anders"
99
+ 3. The ksuid of the object. Ex: "1yPYGaOEPNwr2pfqBgONY43zORp"
100
+
101
+ A couple examples for job_id are:
102
+ - BUILD:linaro@anders#1yPYGaOEPNwr2pCqBgONY43zORq
103
+ - TEST:arm@bob#1yPYGaOEPNwr2pCqBgONY43zORp
104
+
105
+ Then it's up to SQUAD's TuxSuite backend to parse the job_id
106
+ and fetch results properly.
107
+ """
108
+ _type = "TEST" if result_type == "test" else "BUILD"
109
+ project = result["project"].replace("/", "@")
110
+ uid = result["uid"]
111
+ return f"{_type}:{project}#{uid}"
112
+
86
113
  def fetch_url(self, *urlbits):
87
114
  url = reduce(urljoin, urlbits)
88
115
 
89
116
  try:
90
117
  response = requests.get(url)
91
118
  except Exception as e:
92
- raise TemporaryFetchIssue(f"Can't retrieve from {url}: %s" % e)
119
+ raise TemporaryFetchIssue(f"Can't retrieve from {url}: {e}")
93
120
 
94
121
  return response
95
122
 
123
+ def fetch_from_results_input(self, test_job):
124
+ try:
125
+ return json.loads(test_job.input)
126
+ except Exception as e:
127
+ logger.error(f"Can't parse results from job's input: {e}")
128
+
129
+ return None
130
+
96
131
  def parse_build_results(self, test_job, job_url, results, settings):
97
132
  required_keys = ['build_status', 'warnings_count', 'download_url', 'retry']
98
133
  self.__check_required_keys__(required_keys, results)
@@ -179,6 +214,9 @@ class Backend(BaseBackend):
179
214
  _, _, test_id = self.parse_job_id(test_job.job_id)
180
215
  build_id = results['waiting_for']
181
216
  build_url = job_url.replace(test_id, build_id).replace('tests', 'builds')
217
+
218
+ # TODO: check if we can save a few seconds by querying a testjob that
219
+ # already contains build results
182
220
  build_metadata = self.fetch_url(build_url).json()
183
221
 
184
222
  build_metadata_keys = settings.get('TEST_BUILD_METADATA_KEYS', [])
@@ -211,7 +249,12 @@ class Backend(BaseBackend):
211
249
 
212
250
  def fetch(self, test_job):
213
251
  url = self.job_url(test_job)
214
- results = self.fetch_url(url).json()
252
+ if test_job.input:
253
+ results = self.fetch_from_results_input(test_job)
254
+ test_job.input = None
255
+ else:
256
+ results = self.fetch_url(url).json()
257
+
215
258
  if results.get('state') != 'finished':
216
259
  return None
217
260
 
@@ -253,3 +296,49 @@ class Backend(BaseBackend):
253
296
  url = urljoin(self.data.url, endpoint)
254
297
  response = requests.post(url)
255
298
  return response.status_code == 200
299
+
300
+ def supports_callbacks(self):
301
+ return True
302
+
303
+ def validate_callback(self, request, project):
304
+ signature = request.headers.get("x-tux-payload-signature", None)
305
+ if signature is None:
306
+ raise Exception("tuxsuite request is missing signature headers")
307
+
308
+ public_key = project.get_setting("TUXSUITE_PUBLIC_KEY")
309
+ if public_key is None:
310
+ raise Exception("missing tuxsuite public key for this project")
311
+
312
+ payload = request.body
313
+ signature = base64.urlsafe_b64decode(signature)
314
+ key = serialization.load_ssh_public_key(public_key.encode("ascii"))
315
+ key.verify(
316
+ signature,
317
+ payload,
318
+ ec.ECDSA(hashes.SHA256()),
319
+ )
320
+
321
+ def process_callback(self, json_payload, build, environment, backend):
322
+ if "kind" not in json_payload or "status" not in json_payload:
323
+ raise Exception("`kind` and `status` are required in the payload")
324
+
325
+ kind = json_payload["kind"]
326
+ status = json_payload["status"]
327
+ job_id = self.generate_job_id(kind, status)
328
+ try:
329
+ # Tuxsuite's job id DO NOT repeat, like ever
330
+ testjob = TestJob.objects.get(job_id=job_id, target_build=build, environment=environment.slug)
331
+ except TestJob.DoesNotExist:
332
+ testjob = TestJob.objects.create(
333
+ backend=backend,
334
+ target=build.project,
335
+ target_build=build,
336
+ environment=environment.slug,
337
+ submitted=True,
338
+ job_id=job_id
339
+ )
340
+
341
+ # Saves the input so it can be processed by the queue
342
+ testjob.input = json.dumps(status)
343
+
344
+ return testjob
@@ -0,0 +1,22 @@
1
+ # Generated by Django 4.2 on 2023-04-26 16:50
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('ci', '0028_create_testjob_indexes'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='ResultsInput',
16
+ fields=[
17
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('text', models.TextField(blank=True, null=True)),
19
+ ('test_job', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='results_input', to='ci.testjob')),
20
+ ],
21
+ ),
22
+ ]
squad/ci/models.py CHANGED
@@ -164,6 +164,15 @@ class Backend(models.Model):
164
164
  def check_job_definition(self, definition):
165
165
  return self.get_implementation().check_job_definition(definition)
166
166
 
167
+ def supports_callbacks(self):
168
+ return self.get_implementation().supports_callbacks()
169
+
170
+ def validate_callback(self, request, project):
171
+ self.get_implementation().validate_callback(request, project)
172
+
173
+ def process_callback(self, payload, build, environment):
174
+ return self.get_implementation().process_callback(payload, build, environment, self)
175
+
167
176
  def __str__(self):
168
177
  return '%s (%s)' % (self.name, self.implementation_type)
169
178
 
@@ -241,6 +250,21 @@ class TestJob(models.Model):
241
250
  return self.backend.get_implementation().job_url(self)
242
251
  return None
243
252
 
253
+ @property
254
+ def input(self):
255
+ try:
256
+ return self.results_input.text
257
+ except ResultsInput.DoesNotExist:
258
+ return None
259
+
260
+ @input.setter
261
+ def input(self, value):
262
+ if value:
263
+ self.results_input = ResultsInput(text=value)
264
+ self.results_input.save()
265
+ else:
266
+ self.results_input.delete()
267
+
244
268
  def resubmit(self):
245
269
  ret_value = False
246
270
  if self.can_resubmit:
@@ -311,3 +335,8 @@ class TestJob(models.Model):
311
335
  indexes = [
312
336
  models.Index(fields=['submitted', 'fetched']),
313
337
  ]
338
+
339
+
340
+ class ResultsInput(models.Model):
341
+ test_job = models.OneToOneField(TestJob, related_name='results_input', on_delete=models.CASCADE, null=True)
342
+ text = models.TextField(null=True, blank=True)
squad/plugins/github.py CHANGED
@@ -4,6 +4,7 @@ from django.conf import settings
4
4
  from squad.core.models import ProjectStatus
5
5
  from squad.core.plugins import Plugin as BasePlugin
6
6
  from squad.frontend.templatetags.squad import project_url
7
+ from urllib.parse import urljoin
7
8
 
8
9
 
9
10
  def build_url(build):
@@ -11,7 +12,6 @@ def build_url(build):
11
12
 
12
13
 
13
14
  class Plugin(BasePlugin):
14
-
15
15
  @staticmethod
16
16
  def __github_post__(build, endpoint, payload):
17
17
  api_url = build.patch_source.url
@@ -22,10 +22,11 @@ class Plugin(BasePlugin):
22
22
  "Authorization": "token %s" % api_token,
23
23
  }
24
24
 
25
- url = api_url + endpoint.format(
26
- owner=owner,
27
- repository=repository,
28
- commit=commit
25
+ url = urljoin(
26
+ api_url, endpoint.format(
27
+ owner=owner,
28
+ repository=repository,
29
+ commit=commit)
29
30
  )
30
31
  return requests.post(url, headers=headers, json=payload)
31
32
 
squad/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.66'
1
+ __version__ = '1.67'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: squad
3
- Version: 1.66
3
+ Version: 1.67
4
4
  Summary: Software Quality Dashboard
5
5
  Home-page: https://github.com/Linaro/squad
6
6
  Author: Antonio Terceiro
@@ -10,29 +10,29 @@ squad/manage.py,sha256=Z-LXT67p0R-IzwJ9fLIAacEZmU0VUjqDOSg7j2ZSxJ4,1437
10
10
  squad/settings.py,sha256=EzUgd8Egzp6GrZd-Vx6OlAorhlVavT84bxvsqD58oZg,14051
11
11
  squad/socialaccount.py,sha256=vySqPwQ3qVVpahuJ-Snln8K--yzRL3bw4Nx27AsB39A,789
12
12
  squad/urls.py,sha256=JiEfVW8YlzLPE52c2aHzdn5kVVKK4o22w8h5KOA6QhQ,2776
13
- squad/version.py,sha256=UZgUaZ1h7d5_HEpkVjhFfumarSXOI5FTWWBKqnCojyU,21
13
+ squad/version.py,sha256=cyth8cQRyIgeHOgLyFVNnADaoVSn0Rrc5rl9Qs7F8gs,21
14
14
  squad/wsgi.py,sha256=SF8T0cQ0OPVyuYjO5YXBIQzvSXQHV0M2BTmd4gP1rPs,387
15
15
  squad/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  squad/api/apps.py,sha256=Trk72p-iV1uGn0o5mdJn5HARUoHGbfgO49jwXvpkmdQ,141
17
- squad/api/ci.py,sha256=lglQch5iOeGRu6zdjLss2nkPVxer_vDFqBOIlhFMWPs,5114
17
+ squad/api/ci.py,sha256=7eJvUwUbQ7sJzUxcxYURDV1tOT1ouqo0M3L7ojWVAFE,6536
18
18
  squad/api/data.py,sha256=obKDV0-neEvj5lPF9VED2gy_hpfhGtLJABYvSY38ing,2379
19
19
  squad/api/filters.py,sha256=Zvp8DCJmiNquFWqvfVseEAAMYYPiT95RUjqKdzcqSnw,6917
20
20
  squad/api/rest.py,sha256=aVHqq-mXLctPjro29miLqWmR1gMEAIMBeXdbZX-7rQI,74991
21
- squad/api/urls.py,sha256=bud5wHWbHes-klZ7rzBuF7wkKsctDzPxI406I-EwEHI,1202
21
+ squad/api/urls.py,sha256=rmsdaL1uOCVSZ5x1redup9RliICmijaBjRK5ObsTkG8,1343
22
22
  squad/api/utils.py,sha256=Sa8QFId3_oSqD2UOoY3Kuh54LLDLPNMq2sub5ktd6Fs,1160
23
23
  squad/api/views.py,sha256=yGLUp6RtNI5vuae6cOStMuUpSia46LcEVam3eMXmEqY,3885
24
24
  squad/ci/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  squad/ci/admin.py,sha256=7yB-6F0cvt0NVvzGOTlZCyGPV_YHarmbKJZTTzataT4,2255
26
26
  squad/ci/apps.py,sha256=6OVnzTdJkxdqEJnKWYE9dZgUcc29_T1LrDw41cK4EQk,139
27
27
  squad/ci/exceptions.py,sha256=a1sccygniTYDSQi7FRn_6doapddFFiMf55AwGUh5Y80,227
28
- squad/ci/models.py,sha256=1Cwe7RBtftXl7T98GBHzmps8rgqE2prDZJ1RtP4k4zk,11379
28
+ squad/ci/models.py,sha256=RoXJY4Xgu3-YA4FtcK-kem8HR8paqOb5648NByAlzDo,12325
29
29
  squad/ci/tasks.py,sha256=yrtxfPuYEqqGCDYRwLz2XyIp9a7LJ-K6Zm3VS1ymdZk,2728
30
30
  squad/ci/utils.py,sha256=38zHpw8xkZDSFlkG-2BwSK6AkcddK9OkN9LXuQ3SHR0,97
31
31
  squad/ci/backend/__init__.py,sha256=yhpotXT9F4IdAOXvGQ3-17eOHAFwoaqf9SnMX17ab30,534
32
- squad/ci/backend/fake.py,sha256=jVFwriX947DJKplwYD2cKW9pT3p1C7KGwPYROS7wxf0,2103
32
+ squad/ci/backend/fake.py,sha256=zzOXGesDCW9xiMQvXGD_jqCQF32yEd7hPM8DgfZxUok,2159
33
33
  squad/ci/backend/lava.py,sha256=InqmLjf_txjKpMDpmsYbkPJEhOVADBGnTzwaA76fr1Y,33076
34
- squad/ci/backend/null.py,sha256=AzACuhYP0bsZi_OzDEJs_JvkqjczpVjnrgcQLV-OEgg,4356
35
- squad/ci/backend/tuxsuite.py,sha256=Oud5rjvZ99ImNzwne3GfgPbeFGX-USyidRLmcNoqAJI,9555
34
+ squad/ci/backend/null.py,sha256=vP77Xj7roruehSjX8fJs7xK2aWxgaUA2id3P8nHNrEY,4949
35
+ squad/ci/backend/tuxsuite.py,sha256=m4Hgpb0twM4icOrPdEHUgvezmVq9DDvtEloojit_2pg,12759
36
36
  squad/ci/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  squad/ci/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  squad/ci/management/commands/create_tuxsuite_boot_tests.py,sha256=JvjNusebLX71eyz9d-kaeCyekYSpzc1eXoeIqWK9ygo,4045
@@ -66,6 +66,7 @@ squad/ci/migrations/0025_backend_listen_enabled.py,sha256=t7Tx7URhsz-Q4GGuoKYNJY
66
66
  squad/ci/migrations/0026_job_start_end_time.py,sha256=18swRRnDXIhHpr2ykMFns04VS1_Fbs7Zdc0HOrikSSY,543
67
67
  squad/ci/migrations/0027_add_tuxsuite_implementation_type.py,sha256=5Max0wE_jnU5FXdJzbrPGr73N0pdZlA321pZKE-yDtE,502
68
68
  squad/ci/migrations/0028_create_testjob_indexes.py,sha256=VYT2wmrvjHJLOazBXZb15vKzGJn8jgKCVNNSSj4Nct4,612
69
+ squad/ci/migrations/0029_create_testjob_results_input.py,sha256=Ax0jBSthCEDWUp7tLfdCgwa0nXgC2JDXOdQ26Kl9_fA,712
69
70
  squad/ci/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
71
  squad/ci/templates/squad/ci/testjob_resubmit.html.jinja2,sha256=ys8zb4E2CJSsi4-uP57glkOFhWVK5x8-yUZcDdi81sY,1693
71
72
  squad/ci/templates/squad/ci/testjob_resubmit.txt.jinja2,sha256=yKYtixlO5zWh6ChD_mk8yDQly_iQae5slB7a0FwNPv0,610
@@ -422,16 +423,16 @@ squad/frontend/templatetags/squad.py,sha256=YSDgZu8e6e99rFhHudzVKh-6cIfoeq5fC0lW
422
423
  squad/plugins/__init__.py,sha256=9BSzy2jFIoDpWlhD7odPPrLdW4CC3btBhdFCvB651dM,152
423
424
  squad/plugins/example.py,sha256=BKpwd315lHRIuNXJPteibpwfnI6C5eXYHYdFYBtVmsI,89
424
425
  squad/plugins/gerrit.py,sha256=CqO2KnFQzu9utr_TQ-sGr1wg3ln0B-bS2-c0_i8T5-c,7009
425
- squad/plugins/github.py,sha256=e-TbWYcPK1r3HnYj3Y-eQ4zdBW4fRO2eBvWvE4CjxeI,2348
426
+ squad/plugins/github.py,sha256=4ZXhR-eBVdmLUK-dBJVMRHOyjue9oknNrUfkuUqkhY0,2413
426
427
  squad/plugins/linux_log_parser.py,sha256=Dfcvh1-t5380QS_tvNIpzW_UD-4gptT9I4FUHVbb0PU,5503
427
428
  squad/run/__init__.py,sha256=ssE8GPAGFiK6V0WpZYowav6Zqsd63dfDMMYasNa1sQg,1410
428
429
  squad/run/__main__.py,sha256=DOl8JOi4Yg7DdtwnUeGqtYBJ6P2k-D2psAEuYOjWr8w,66
429
430
  squad/run/listener.py,sha256=jBeOQhPGb4EdIREB1QsCzYuumsfJ-TqJPd3nR-0m59g,200
430
431
  squad/run/scheduler.py,sha256=CDJG3q5C0GuQuxwlMOfWTSSJpDdwbR6rzpbJfuA0xuw,277
431
432
  squad/run/worker.py,sha256=jtML0h5qKDuSbpJ6_rpWP4MT_rsGA7a24AhwGxBquzk,594
432
- squad-1.66.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
433
- squad-1.66.dist-info/METADATA,sha256=5oU0z0GN7cXjL6N6b5yV7ipON8CHroonQehF5rMf5Vo,1267
434
- squad-1.66.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
435
- squad-1.66.dist-info/entry_points.txt,sha256=apCDQydHZtvqV334ql6NhTJUAJeZRdtAm0TVcbbAi5Q,194
436
- squad-1.66.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
437
- squad-1.66.dist-info/RECORD,,
433
+ squad-1.67.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
434
+ squad-1.67.dist-info/METADATA,sha256=PA2cyjXXMPX2KVIyIzo6xcLtGugZFDHfLmqvpN8TEvU,1267
435
+ squad-1.67.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
436
+ squad-1.67.dist-info/entry_points.txt,sha256=apCDQydHZtvqV334ql6NhTJUAJeZRdtAm0TVcbbAi5Q,194
437
+ squad-1.67.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
438
+ squad-1.67.dist-info/RECORD,,
File without changes
File without changes