destiny_sdk 0.7.1__py3-none-any.whl → 0.7.3__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.
destiny_sdk/client.py CHANGED
@@ -1,11 +1,22 @@
1
1
  """Send authenticated requests to Destiny Repository."""
2
2
 
3
+ import sys
3
4
  import time
4
5
  from collections.abc import Generator
5
6
 
6
7
  import httpx
7
- from pydantic import UUID4, HttpUrl
8
+ from msal import (
9
+ ConfidentialClientApplication,
10
+ ManagedIdentityClient,
11
+ PublicClientApplication,
12
+ UserAssignedManagedIdentity,
13
+ )
14
+ from pydantic import UUID4, HttpUrl, TypeAdapter
8
15
 
16
+ from destiny_sdk.auth import create_signature
17
+ from destiny_sdk.core import sdk_version
18
+ from destiny_sdk.identifiers import IdentifierLookup
19
+ from destiny_sdk.references import Reference, ReferenceSearchResult
9
20
  from destiny_sdk.robots import (
10
21
  EnhancementRequestRead,
11
22
  RobotEnhancementBatch,
@@ -13,8 +24,10 @@ from destiny_sdk.robots import (
13
24
  RobotEnhancementBatchResult,
14
25
  RobotResult,
15
26
  )
27
+ from destiny_sdk.search import AnnotationFilter
16
28
 
17
- from .auth import create_signature
29
+ python_version = ".".join(map(str, sys.version_info[:3]))
30
+ user_agent = f"python@{python_version}/destiny-sdk@{sdk_version}"
18
31
 
19
32
 
20
33
  class HMACSigningAuth(httpx.Auth):
@@ -53,7 +66,7 @@ class HMACSigningAuth(httpx.Auth):
53
66
  yield request
54
67
 
55
68
 
56
- class Client:
69
+ class RobotClient:
57
70
  """
58
71
  Client for interaction with the Destiny API.
59
72
 
@@ -71,7 +84,10 @@ class Client:
71
84
  """
72
85
  self.session = httpx.Client(
73
86
  base_url=str(base_url).removesuffix("/").removesuffix("/v1") + "/v1",
74
- headers={"Content-Type": "application/json"},
87
+ headers={
88
+ "Content-Type": "application/json",
89
+ "User-Agent": user_agent,
90
+ },
75
91
  auth=HMACSigningAuth(secret_key=secret_key, client_id=client_id),
76
92
  )
77
93
 
@@ -172,3 +188,414 @@ class Client:
172
188
  params={"lease": lease_duration} if lease_duration else None,
173
189
  )
174
190
  response.raise_for_status()
191
+
192
+
193
+ # Backward compatibility
194
+ Client = RobotClient
195
+
196
+
197
+ class OAuthMiddleware(httpx.Auth):
198
+ """
199
+ Auth middleware that handles OAuth2 token retrieval and refresh.
200
+
201
+ This is generally used in conjunction with
202
+ :class:`OAuthClient <libs.sdk.src.destiny_sdk.client.OAuthClient>`.
203
+
204
+ Supports three authentication flows:
205
+
206
+ **Public Client Application (human login)**
207
+
208
+ Initial login will be interactive through a browser window. Subsequent token
209
+ retrievals will use cached tokens and refreshes where possible, and only prompt
210
+ for login again if necessary.
211
+
212
+ .. code-block:: python
213
+
214
+ auth = OAuthMiddleware(
215
+ azure_client_id="client-id",
216
+ azure_application_id="login-url",
217
+ azure_tenant_id="tenant-id",
218
+ )
219
+
220
+ **Confidential Client Application (client credentials)**
221
+
222
+ Suitable for service-to-service authentication where no user interaction is
223
+ possible or desired. Reach out if you need help setting up a confidential client
224
+ application. The secret must be stored securely.
225
+
226
+ .. code-block:: python
227
+
228
+ auth = OAuthMiddleware(
229
+ azure_client_id="client-id",
230
+ azure_application_id="application-id",
231
+ azure_login_url="login-url",
232
+ azure_client_secret="your-azure-client-secret",
233
+ )
234
+
235
+ **Azure Managed Identity**
236
+
237
+ Suitable for Azure environments that have had API permissions provisioned for
238
+ their managed identity. Note that the ``azure_client_id`` here is the client ID of
239
+ the managed identity, not the repository.
240
+
241
+ .. code-block:: python
242
+
243
+ auth = OAuthMiddleware(
244
+ azure_client_id="your-managed-identity-client-id",
245
+ azure_application_id="application-id",
246
+ use_managed_identity=True,
247
+ )
248
+
249
+ """
250
+
251
+ def __init__(
252
+ self,
253
+ azure_client_id: str,
254
+ azure_application_id: str,
255
+ azure_login_url: HttpUrl | str | None = None,
256
+ azure_client_secret: str | None = None,
257
+ *,
258
+ use_managed_identity: bool = False,
259
+ ) -> None:
260
+ """
261
+ Initialize the auth middleware.
262
+
263
+ :param tenant_id: The OAuth2 tenant ID.
264
+ :type tenant_id: str
265
+ :param client_id: The OAuth2 client ID.
266
+ :type client_id: str
267
+ :param application_id: The application ID for the Destiny API.
268
+ :type application_id: str
269
+ :param azure_login_url: The Azure login URL.
270
+ :type azure_login_url: str
271
+ :param azure_client_secret: The Azure client secret.
272
+ :type azure_client_secret: str | None
273
+ :param use_managed_identity: Whether to use managed identity for authentication
274
+ :type use_managed_identity: bool
275
+ """
276
+ if use_managed_identity:
277
+ if (
278
+ any(
279
+ [
280
+ azure_login_url,
281
+ azure_client_secret,
282
+ ]
283
+ )
284
+ or not azure_client_id
285
+ ):
286
+ msg = (
287
+ "azure_login_url and azure_client_secret must not be provided "
288
+ "when using managed identity authentication"
289
+ )
290
+ raise ValueError(msg)
291
+ self._oauth_app = ManagedIdentityClient(
292
+ UserAssignedManagedIdentity(client_id=azure_client_id),
293
+ http_client=httpx.Client(),
294
+ )
295
+ self._get_token = self._get_token_from_managed_identity
296
+ elif azure_client_secret:
297
+ if not azure_login_url:
298
+ msg = (
299
+ "azure_login_url must be provided "
300
+ "when not using managed identity authentication"
301
+ )
302
+ raise ValueError(msg)
303
+ self._oauth_app = ConfidentialClientApplication(
304
+ client_id=azure_client_id,
305
+ authority=str(azure_login_url),
306
+ client_credential=azure_client_secret,
307
+ )
308
+ self._get_token = self._get_token_from_confidential_client
309
+ else:
310
+ if not azure_login_url:
311
+ msg = (
312
+ "azure_login_url must be provided "
313
+ "when not using managed identity authentication"
314
+ )
315
+ raise ValueError(msg)
316
+ self._oauth_app = PublicClientApplication(
317
+ azure_client_id,
318
+ authority=str(azure_login_url),
319
+ client_credential=None,
320
+ )
321
+ self._get_token = self._get_token_from_public_client
322
+
323
+ self._scope = f"api://{azure_application_id}/.default"
324
+ self._account = None
325
+
326
+ def _parse_token(self, msal_response: dict) -> str:
327
+ """
328
+ Parse the OAuth2 token from an MSAL response.
329
+
330
+ :param msal_response: The MSAL response containing the token.
331
+ :type msal_response: dict
332
+ :return: The OAuth2 token.
333
+ :rtype: str
334
+ """
335
+ if not msal_response.get("access_token"):
336
+ msg = (
337
+ "Failed to acquire access token: "
338
+ f"{msal_response.get('error', 'Unknown error')}"
339
+ )
340
+ raise RuntimeError(msg)
341
+
342
+ return msal_response["access_token"]
343
+
344
+ def _get_token_from_public_client(self, *, force_refresh: bool = False) -> str:
345
+ """
346
+ Get an OAuth2 token from a PublicClientApplication.
347
+
348
+ :param force_refresh: Whether to force a token refresh.
349
+ :type force_refresh: bool
350
+ :return: The OAuth2 token.
351
+ :rtype: str
352
+ """
353
+ if not isinstance(self._oauth_app, PublicClientApplication):
354
+ msg = "oauth_app must be a PublicClientApplication for this method"
355
+ raise TypeError(msg)
356
+
357
+ # Uses msal cache if possible, else interactive login
358
+ result = self._oauth_app.acquire_token_silent(
359
+ scopes=[self._scope],
360
+ account=self._account,
361
+ force_refresh=force_refresh,
362
+ )
363
+ if not result:
364
+ result = self._oauth_app.acquire_token_interactive(scopes=[self._scope])
365
+
366
+ access_token = self._parse_token(result)
367
+
368
+ # After first login, cache the account for silent token acquisition
369
+ if not self._account and (accounts := self._oauth_app.get_accounts()):
370
+ self._account = accounts[0]
371
+
372
+ return access_token
373
+
374
+ def _get_token_from_confidential_client(
375
+ self,
376
+ *,
377
+ force_refresh: bool = False, # noqa: ARG002 MSAL will handle refreshing
378
+ ) -> str:
379
+ """
380
+ Get an OAuth2 token from a ConfidentialClientApplication.
381
+
382
+ :param force_refresh: Whether to force a token refresh.
383
+ :type force_refresh: bool
384
+ :return: The OAuth2 token.
385
+ :rtype: str
386
+ """
387
+ if not isinstance(self._oauth_app, ConfidentialClientApplication):
388
+ msg = "oauth_app must be a ConfidentialClientApplication for this method"
389
+ raise TypeError(msg)
390
+
391
+ # Uses msal cache if possible, else client credentials flow
392
+ result = self._oauth_app.acquire_token_for_client(scopes=[self._scope])
393
+
394
+ return self._parse_token(result)
395
+
396
+ def _get_token_from_managed_identity(
397
+ self,
398
+ *,
399
+ force_refresh: bool = False, # noqa: ARG002 MSAL will handle refreshing
400
+ ) -> str:
401
+ """
402
+ Get an OAuth2 token from a ManagedIdentityClient.
403
+
404
+ :param force_refresh: Whether to force a token refresh.
405
+ :type force_refresh: bool
406
+ :return: The OAuth2 token.
407
+ :rtype: str
408
+ """
409
+ if not isinstance(self._oauth_app, ManagedIdentityClient):
410
+ msg = "oauth_app must be a ManagedIdentityClient for this method"
411
+ raise TypeError(msg)
412
+
413
+ result = self._oauth_app.acquire_token_for_client(
414
+ resource=self._scope.removesuffix("/.default")
415
+ )
416
+
417
+ return self._parse_token(result)
418
+
419
+ def auth_flow(
420
+ self, request: httpx.Request
421
+ ) -> Generator[httpx.Request, httpx.Response]:
422
+ """
423
+ Add OAuth2 token to request and handle token refresh on expiration.
424
+
425
+ :param request: The request to authenticate.
426
+ :type request: httpx.Request
427
+ :yield: Authenticated request with token refresh handling.
428
+ :rtype: Generator[httpx.Request, httpx.Response]
429
+ """
430
+ # Add initial token
431
+ token = self._get_token()
432
+ request.headers["Authorization"] = f"Bearer {token}"
433
+
434
+ response = yield request
435
+
436
+ # Check if token expired and retry once with fresh token
437
+ if response.status_code == httpx.codes.UNAUTHORIZED:
438
+ try:
439
+ json_response: dict = response.json()
440
+ error_detail: str = json_response.get("detail", {})
441
+ except ValueError:
442
+ error_detail = ""
443
+
444
+ if error_detail == "Token has expired.":
445
+ # Force refresh token and retry
446
+ token = self._get_token(force_refresh=True)
447
+ request.headers["Authorization"] = f"Bearer {token}"
448
+ yield request
449
+
450
+
451
+ class OAuthClient:
452
+ """
453
+ Client for interaction with the Destiny API using OAuth2.
454
+
455
+ This will apply the provided authentication, usually
456
+ :class:`OAuthMiddleware <libs.sdk.src.destiny_sdk.client.OAuthMiddleware>`,
457
+ to all requests. Some API endpoints are supported directly through methods on this
458
+ class, while others can be accessed through the underlying ``httpx`` client.
459
+
460
+ Example usage:
461
+
462
+ .. code-block:: python
463
+
464
+ from destiny_sdk.client import OAuthClient, OAuthMiddleware
465
+
466
+ client = OAuthClient(
467
+ base_url="https://destiny-repository.example.com",
468
+ auth=OAuthMiddleware(...),
469
+ )
470
+
471
+ # Supported method
472
+ response = client.search(query="example")
473
+
474
+ # Unsupported method, use underlying httpx client
475
+ response = client.get_client().get("/system/healthcheck/")
476
+ """
477
+
478
+ def __init__(
479
+ self,
480
+ base_url: HttpUrl | str,
481
+ auth: httpx.Auth | None = None,
482
+ ) -> None:
483
+ """
484
+ Initialize the client.
485
+
486
+ :param base_url: The base URL for the Destiny Repository API.
487
+ :type base_url: HttpUrl
488
+ :param auth: The middleware for authentication. If not provided, only
489
+ unauthenticated requests can be made. This should almost always be an
490
+ instance of ``OAuthMiddleware``, unless you need to create a custom auth
491
+ class.
492
+ :type auth: httpx.Auth | None
493
+ """
494
+ self._client = httpx.Client(
495
+ base_url=str(base_url).removesuffix("/").removesuffix("/v1") + "/v1",
496
+ headers={
497
+ "Content-Type": "application/json",
498
+ "User-Agent": user_agent,
499
+ },
500
+ )
501
+
502
+ if auth:
503
+ self._client.auth = auth
504
+
505
+ def _raise_for_status(self, response: httpx.Response) -> None:
506
+ """
507
+ Raise an error if the response status is not successful.
508
+
509
+ :param response: The HTTP response to check.
510
+ :type response: httpx.Response
511
+ :raises httpx.HTTPStatusError: If the response status is not successful.
512
+ """
513
+ try:
514
+ response.raise_for_status()
515
+ except httpx.HTTPStatusError as exc:
516
+ msg = (
517
+ f"Error response {exc.response.status_code} from "
518
+ f"{exc.request.url}: {exc.response.text}"
519
+ )
520
+ raise httpx.HTTPStatusError(
521
+ msg, request=exc.request, response=exc.response
522
+ ) from exc
523
+
524
+ def search( # noqa: PLR0913
525
+ self,
526
+ query: str,
527
+ start_year: int | None = None,
528
+ end_year: int | None = None,
529
+ annotations: list[str | AnnotationFilter] | None = None,
530
+ sort: str | None = None,
531
+ page: int = 1,
532
+ ) -> ReferenceSearchResult:
533
+ """
534
+ Send a search request to the Destiny Repository API.
535
+
536
+ See also: :ref:`search-procedure`.
537
+
538
+ :param query: The search query string.
539
+ :type query: str
540
+ :param start_year: The start year for filtering results.
541
+ :type start_year: int | None
542
+ :param end_year: The end year for filtering results.
543
+ :type end_year: int | None
544
+ :param annotations: A list of annotation filters to apply.
545
+ :type annotations: list[str | libs.sdk.src.destiny_sdk.search.AnnotationFilter] | None
546
+ :param sort: The sort order for the results.
547
+ :type sort: str | None
548
+ :param page: The page number of results to retrieve.
549
+ :type page: int
550
+ :return: The response from the API.
551
+ :rtype: libs.sdk.src.destiny_sdk.references.ReferenceSearchResult
552
+ """ # noqa: E501
553
+ params = {"q": query, "page": page}
554
+ if start_year:
555
+ params["start_year"] = start_year
556
+ if end_year:
557
+ params["end_year"] = end_year
558
+ if annotations:
559
+ params["annotation"] = [str(annotation) for annotation in annotations]
560
+ if sort:
561
+ params["sort"] = sort
562
+ response = self._client.get(
563
+ "/references/search/",
564
+ params=params,
565
+ )
566
+ self._raise_for_status(response)
567
+ return ReferenceSearchResult.model_validate(response.json())
568
+
569
+ def lookup(
570
+ self,
571
+ identifiers: list[str | IdentifierLookup],
572
+ ) -> list[Reference]:
573
+ """
574
+ Lookup references by identifiers.
575
+
576
+ See also: :ref:`lookup-procedure`.
577
+
578
+ :param identifiers: The identifiers to look up.
579
+ :type identifiers: list[str | libs.sdk.src.destiny_sdk.identifiers.IdentifierLookup]
580
+ :return: The list of references matching the identifiers.
581
+ :rtype: list[libs.sdk.src.destiny_sdk.references.Reference]
582
+ """ # noqa: E501
583
+ response = self._client.get(
584
+ "/references/",
585
+ params={
586
+ "identifier": ",".join([str(identifier) for identifier in identifiers])
587
+ },
588
+ )
589
+ self._raise_for_status(response)
590
+ return TypeAdapter(list[Reference]).validate_python(response.json())
591
+
592
+ def get_client(self) -> httpx.Client:
593
+ """
594
+ Get the underlying ``httpx`` client.
595
+
596
+ This can be used to make custom requests not covered by the SDK methods.
597
+
598
+ :return: The underlying ``httpx`` client with authentication attached.
599
+ :rtype: `httpx.Client <https://www.python-httpx.org/advanced/clients/>`_
600
+ """
601
+ return self._client
destiny_sdk/core.py CHANGED
@@ -1,11 +1,18 @@
1
1
  """Core classes for the Destiny SDK, not exposed to package users."""
2
2
 
3
+ from importlib.metadata import PackageNotFoundError, version
3
4
  from typing import Self
4
5
 
5
6
  from pydantic import BaseModel, Field
6
7
 
7
8
  from destiny_sdk.search import SearchResultPage, SearchResultTotal
8
9
 
10
+ try:
11
+ sdk_version = version("destiny-sdk")
12
+ except PackageNotFoundError:
13
+ sdk_version = "unknown"
14
+
15
+
9
16
  # These are non-standard newline characters that are not escaped by model_dump_json().
10
17
  # We want jsonl files to have empirical new lines so they can be streamed line by line.
11
18
  # Hence we replace each occurrence with standard new lines.
@@ -1,6 +1,7 @@
1
1
  """Enhancement classes for the Destiny Repository."""
2
2
 
3
3
  import datetime
4
+ import json
4
5
  from enum import StrEnum, auto
5
6
  from typing import Annotated, Any, Literal, Self
6
7
 
@@ -87,6 +88,10 @@ other works have cited this work
87
88
  created_date: datetime.date | None = Field(
88
89
  default=None, description="The ISO8601 date this metadata record was created"
89
90
  )
91
+ updated_date: datetime.date | None = Field(
92
+ default=None,
93
+ description="The ISO8601 date of the last OpenAlex update to this metadata",
94
+ )
90
95
  publication_date: datetime.date | None = Field(
91
96
  default=None, description="The date which the version of record was published."
92
97
  )
@@ -100,6 +105,20 @@ other works have cited this work
100
105
  )
101
106
  title: str | None = Field(default=None, description="The title of the reference.")
102
107
 
108
+ @property
109
+ def fingerprint(self) -> str:
110
+ """
111
+ The fingerprint of this bibliographic metadata enhancement.
112
+
113
+ Excludes updated_at from the fingerprint calculation, meaning
114
+ that two raw enhancements with identical data but different export dates
115
+ will be considered the same.
116
+ """
117
+ return json.dumps(
118
+ self.model_dump(mode="json", exclude={"updated_date"}, exclude_none=True),
119
+ sort_keys=True,
120
+ )
121
+
103
122
 
104
123
  class AbstractProcessType(StrEnum):
105
124
  """The process used to acquire the abstract."""
@@ -332,6 +351,25 @@ class RawEnhancement(BaseModel):
332
351
  raise ValueError(msg)
333
352
  return self
334
353
 
354
+ @property
355
+ def fingerprint(self) -> str:
356
+ """
357
+ The unique fingerprint of this raw enhancement.
358
+
359
+ Excludes the source_export_date from the fingerprint calculation, meaning
360
+ that two raw enhancements with identical data but different export dates
361
+ will be considered the same.
362
+
363
+ Unstructured data in `data` and `metadata` is included in the fingerprint,
364
+ sorted by key.
365
+ """
366
+ return json.dumps(
367
+ self.model_dump(
368
+ mode="json", exclude={"source_export_date"}, exclude_none=True
369
+ ),
370
+ sort_keys=True,
371
+ )
372
+
335
373
 
336
374
  #: Union type for all enhancement content types.
337
375
  EnhancementContent = Annotated[
@@ -260,3 +260,11 @@ class IdentifierLookup(BaseModel):
260
260
  if self.identifier_type is None:
261
261
  return UUID4(self.identifier)
262
262
  return ExternalIdentifierAdapter.validate_python(self.model_dump())
263
+
264
+ def __repr__(self) -> str:
265
+ """Serialize the identifier lookup to a string."""
266
+ return self.serialize()
267
+
268
+ def __str__(self) -> str:
269
+ """Serialize the identifier lookup to a string."""
270
+ return self.serialize()
destiny_sdk/search.py CHANGED
@@ -51,3 +51,7 @@ class AnnotationFilter(BaseModel):
51
51
  if self.score is not None:
52
52
  annotation += f"@{self.score}"
53
53
  return annotation
54
+
55
+ def __str__(self) -> str:
56
+ """Serialize the annotation filter to a string."""
57
+ return repr(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: destiny_sdk
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: A software development kit (sdk) to support interaction with the DESTINY repository
5
5
  Author-email: Adam Hamilton <adam@futureevidence.org>, Andrew Harvey <andrew@futureevidence.org>, Daniel Breves <daniel@futureevidence.org>, Jack Walmisley <jack@futureevidence.org>, Tim Repke <tim.repke@pik-potsdam.de>
6
6
  License-Expression: Apache-2.0
@@ -9,6 +9,7 @@ Requires-Python: ~=3.12
9
9
  Requires-Dist: cachetools<6,>=5.5.2
10
10
  Requires-Dist: fastapi<0.116,>=0.115.12
11
11
  Requires-Dist: httpx<0.29,>=0.28.1
12
+ Requires-Dist: msal>=1.34.0
12
13
  Requires-Dist: pydantic<3,>=2.11.3
13
14
  Requires-Dist: pytest-asyncio<2,>=1.0.0
14
15
  Requires-Dist: pytest-httpx<0.36,>=0.35.0
@@ -1,21 +1,21 @@
1
1
  destiny_sdk/__init__.py,sha256=NdSlsPQyDF3TW30_JzbvYMRBRA9iT677iTRWWCMdYOA,382
2
2
  destiny_sdk/auth.py,sha256=bY72ywZEcG_67YBd9PrwgWTXkCf58rhLvVEXrtXbWtA,6247
3
- destiny_sdk/client.py,sha256=LoXEBPxekbT-Y8eiTt_Gfy4G5RPj1tURZHuH-V9CLXs,6247
4
- destiny_sdk/core.py,sha256=PYCYpY72MHXo7iQMHtnXcnCOGn6CUsbYoykHvtQl4Oc,1857
5
- destiny_sdk/enhancements.py,sha256=-4jLm3R0T5UpgCt09CgUfPcnzPOyjdhUZCT1zhEP6sQ,12838
6
- destiny_sdk/identifiers.py,sha256=r2dFBIv2vtOK-C5lvHryEOqQBQ6_Odehipc6YgMZVBk,9482
3
+ destiny_sdk/client.py,sha256=nKvS5rRkIpBqv8dVIB57Xsop0UvVz3i875RQxfVSMao,21306
4
+ destiny_sdk/core.py,sha256=E0Wotu9psggK1JRJxbvx3Jc7WEGE6zaz2R2awvRrLz8,2023
5
+ destiny_sdk/enhancements.py,sha256=P_kH59WoWuYq444xjV05HNXlTGfB-lE0ffKYwpAY7z8,14118
6
+ destiny_sdk/identifiers.py,sha256=I9Q2I35Lg8oyl3uytq1gCGOUu92F9sZSSwzLoCXBJi4,9727
7
7
  destiny_sdk/imports.py,sha256=b-rh-dt3NsyLGxqmVzIzKaHiXhbw-3wtAaBN-ZW-i1E,5940
8
8
  destiny_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  destiny_sdk/references.py,sha256=3Y8gBMTSyZY35S3pB1bnVHMai9RRiGeoGZysNvSo7kk,2553
10
10
  destiny_sdk/robots.py,sha256=I_ZvMxwST52e8ovhv0-gPbOB3P9tptbRG0LrkNNOqKo,13463
11
- destiny_sdk/search.py,sha256=RAmUBS2KE2fmzLTxB0jV5R3AeuBrOJAWqieGv4GgFAo,1474
11
+ destiny_sdk/search.py,sha256=QWQBNEJJnH2o6CGapChKUp6kOZl2Uq3Pxqg1kcu9x-4,1590
12
12
  destiny_sdk/visibility.py,sha256=8D44Q868YdScAt6eAFgXXrhonozXnv_Qa5w5yEGMPX8,577
13
13
  destiny_sdk/labs/__init__.py,sha256=H4RFPyeelqZ56PagnWPX-JZeWlxPnCZoYHtr4c9SU9Q,180
14
14
  destiny_sdk/labs/references.py,sha256=iZisRgGZ5c7X7uTFoe6Q0AwwFMa4yJbIoPUVv_hvOiU,5589
15
15
  destiny_sdk/parsers/__init__.py,sha256=d5gS--bXla_0I7e_9wTBnGWMXt2U8b-_ndeprTPe1hk,149
16
16
  destiny_sdk/parsers/eppi_parser.py,sha256=_1xnAT0F0o1HKpMWOGQbVS3VPOrhPqyzHDWR3CosWwk,9484
17
17
  destiny_sdk/parsers/exceptions.py,sha256=0Sc_M4j560Nqh4SjeP_YrgOUVagdIwWwRz24E6YlZ1k,573
18
- destiny_sdk-0.7.1.dist-info/METADATA,sha256=w_3Zbj91oWcz1uCgSN1JLYVTisV1m3PJBio2g9AtopY,2657
19
- destiny_sdk-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
- destiny_sdk-0.7.1.dist-info/licenses/LICENSE,sha256=6QURU4gvvTjVZ5rfp5amZ6FtFvcpPhAGUjxF5WSZAHI,9138
21
- destiny_sdk-0.7.1.dist-info/RECORD,,
18
+ destiny_sdk-0.7.3.dist-info/METADATA,sha256=2z_JjoZa-UMvOaPefJoYAc1ILx39gT0IWIqfYljbS1g,2685
19
+ destiny_sdk-0.7.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
+ destiny_sdk-0.7.3.dist-info/licenses/LICENSE,sha256=6QURU4gvvTjVZ5rfp5amZ6FtFvcpPhAGUjxF5WSZAHI,9138
21
+ destiny_sdk-0.7.3.dist-info/RECORD,,