willisapi-client 1.9.8__tar.gz → 2.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 (33) hide show
  1. {willisapi_client-1.9.8 → willisapi_client-2.0}/PKG-INFO +35 -2
  2. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/__version__.py +1 -1
  3. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/metadata/upload.py +15 -6
  4. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/metadata/utils.py +45 -6
  5. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client.egg-info/PKG-INFO +36 -3
  6. {willisapi_client-1.9.8 → willisapi_client-2.0}/LICENSE +0 -0
  7. {willisapi_client-1.9.8 → willisapi_client-2.0}/MANIFEST.in +0 -0
  8. {willisapi_client-1.9.8 → willisapi_client-2.0}/README.md +0 -0
  9. {willisapi_client-1.9.8 → willisapi_client-2.0}/RELEASE.md +0 -0
  10. {willisapi_client-1.9.8 → willisapi_client-2.0}/requirements.txt +0 -0
  11. {willisapi_client-1.9.8 → willisapi_client-2.0}/setup.cfg +0 -0
  12. {willisapi_client-1.9.8 → willisapi_client-2.0}/setup.py +0 -0
  13. {willisapi_client-1.9.8 → willisapi_client-2.0}/tests/test_diarize.py +0 -0
  14. {willisapi_client-1.9.8 → willisapi_client-2.0}/tests/test_diarize_call_remaining.py +0 -0
  15. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/__init__.py +0 -0
  16. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/logging_setup.py +0 -0
  17. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/__init__.py +0 -0
  18. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/api.py +0 -0
  19. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/diarize/__init__.py +0 -0
  20. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/diarize/diarize_utils.py +0 -0
  21. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/diarize/willisdiarize.py +0 -0
  22. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/diarize/willisdiarize_call_remaining.py +0 -0
  23. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/exceptions.py +0 -0
  24. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/metadata/__init__.py +0 -0
  25. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/metadata/archive.py +0 -0
  26. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/services/metadata/language_choices.py +0 -0
  27. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/timer.py +0 -0
  28. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client/willisapi_client.py +0 -0
  29. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client.egg-info/SOURCES.txt +0 -0
  30. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client.egg-info/dependency_links.txt +0 -0
  31. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client.egg-info/not-zip-safe +0 -0
  32. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client.egg-info/requires.txt +0 -0
  33. {willisapi_client-1.9.8 → willisapi_client-2.0}/willisapi_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: willisapi_client
3
- Version: 1.9.8
3
+ Version: 2.0
4
4
  Summary: A Python client for willisapi
5
5
  Home-page: https://github.com/bklynhlth/willsiapi_client
6
6
  Author: bklynhlth
@@ -9,6 +9,39 @@ License: Apache
9
9
  Requires-Python: >=3.9
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
+ Requires-Dist: aiohttp==3.9.0
13
+ Requires-Dist: aiosignal==1.3.1
14
+ Requires-Dist: async-timeout==4.0.2
15
+ Requires-Dist: asyncio==3.4.3
16
+ Requires-Dist: attrs==23.1.0
17
+ Requires-Dist: certifi==2023.7.22
18
+ Requires-Dist: charset-normalizer==3.2.0
19
+ Requires-Dist: frozenlist==1.4.0
20
+ Requires-Dist: idna==3.4
21
+ Requires-Dist: multidict==6.0.4
22
+ Requires-Dist: nest-asyncio==1.5.7
23
+ Requires-Dist: numpy==1.25.2
24
+ Requires-Dist: pandas==2.0.3
25
+ Requires-Dist: pytest==7.4.0
26
+ Requires-Dist: pytest-cov==4.1.0
27
+ Requires-Dist: python-dateutil==2.8.2
28
+ Requires-Dist: pytz==2023.3
29
+ Requires-Dist: requests==2.31.0
30
+ Requires-Dist: six==1.16.0
31
+ Requires-Dist: tqdm==4.66.1
32
+ Requires-Dist: tzdata==2023.3
33
+ Requires-Dist: urllib3==2.0.7
34
+ Requires-Dist: yarl==1.9.2
35
+ Dynamic: author
36
+ Dynamic: author-email
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: license
41
+ Dynamic: license-file
42
+ Dynamic: requires-dist
43
+ Dynamic: requires-python
44
+ Dynamic: summary
12
45
 
13
46
  <!-- Output copied to clipboard! -->
14
47
 
@@ -1,7 +1,7 @@
1
1
  """Version details for willisapi_client"""
2
2
 
3
3
  __client__ = "willisapi_client"
4
- __latestVersion__ = "1.9.8"
4
+ __latestVersion__ = "2.0"
5
5
  __url__ = "https://github.com/bklynhlth/willsiapi_client"
6
6
  __short_description__ = "A Python client for willisapi"
7
7
  __content_type__ = "text/markdown"
@@ -7,9 +7,16 @@ import os
7
7
  import time
8
8
  from concurrent.futures import ThreadPoolExecutor
9
9
 
10
- # Register extensions that Python's default mimetypes table doesn't know about.
11
- # The server signs presigned URLs with these types, so the client must match.
12
- mimetypes.add_type("application/vnd.apache.parquet", ".parquet")
10
+ # The server signs presigned URLs with Content-Types guessed from Python's
11
+ # built-in mimetypes table (on Linux), so the client must produce identical
12
+ # guesses. A private MimeTypes instance holds only that built-in table —
13
+ # the module-level mimetypes.guess_type() on Windows also reads the registry
14
+ # (e.g. .csv -> application/vnd.ms-excel, .wav -> audio/wav), which makes the
15
+ # PUT's Content-Type differ from the signed one and S3 reject the upload with
16
+ # SignatureDoesNotMatch. Also register extensions the built-in table lacks,
17
+ # mirroring the server.
18
+ _MIME_TYPES = mimetypes.MimeTypes()
19
+ _MIME_TYPES.add_type("application/vnd.apache.parquet", ".parquet")
13
20
 
14
21
  from willisapi_client.timer import measure
15
22
  from willisapi_client.willisapi_client import WillisapiClient
@@ -41,9 +48,11 @@ def _put_file_to_s3(file_presigned: dict):
41
48
  checksum = file_presigned.get("checksum")
42
49
  recording = file_presigned.get("recording")
43
50
  try:
44
- content_type, _ = mimetypes.guess_type(recording)
51
+ content_type, _ = _MIME_TYPES.guess_type(recording)
45
52
  if not content_type:
46
- content_type = "text/csv"
53
+ # Mirror the server's fallback — it signs the presigned URL with
54
+ # this Content-Type when the extension is unknown.
55
+ content_type = "application/octet-stream"
47
56
  with open(recording, "rb") as f:
48
57
  session = build_retry_session()
49
58
  response = session.put(
@@ -108,7 +117,7 @@ def upload(api_key: str, csv_path: str, **kwargs):
108
117
  presigned = res.get("response", {}).get("presigned")
109
118
  if presigned:
110
119
  try:
111
- content_type, _ = mimetypes.guess_type(
120
+ content_type, _ = _MIME_TYPES.guess_type(
112
121
  payload.get("filename")
113
122
  )
114
123
  if not content_type:
@@ -3,6 +3,7 @@ import numpy as np
3
3
  from typing import List, Dict, Any, Tuple
4
4
  import json
5
5
  import os
6
+ import posixpath
6
7
  import hashlib
7
8
  import base64
8
9
  import requests
@@ -472,15 +473,48 @@ class UploadUtils:
472
473
  except Exception as e:
473
474
  raise RuntimeError(f"Failed to calculate checksum: {e}")
474
475
 
476
+ def optional_value(self, column: str):
477
+ """Return a column's value only when the row actually supplies one.
478
+
479
+ Optional columns may be missing from the CSV entirely, or present but
480
+ empty on a given row -- pandas surfaces both as an absent attribute or
481
+ NaN. Passing that straight through would post the string "nan" to the
482
+ API and overwrite good data, so it is dropped instead. Callers omit
483
+ the key altogether, which the API treats as "leave this field alone".
484
+ """
485
+ value = getattr(self.row, column, None)
486
+ if value is None or pd.isna(value):
487
+ return None
488
+ text = str(value).strip()
489
+ return text or None
490
+
491
+ def demographic_fields(self) -> Dict[str, Any]:
492
+ """Optional demographics to send, omitting any the row leaves blank."""
493
+ supplied = {}
494
+ for column in ("sex", "race"):
495
+ value = self.optional_value(column)
496
+ if value is not None:
497
+ supplied[column] = value
498
+
499
+ age = self.optional_value("age")
500
+ if age is not None:
501
+ try:
502
+ # Via float first: pandas widens an int column to float as soon
503
+ # as any row is blank, so a whole-number age arrives as "42.0".
504
+ supplied["age"] = int(float(age))
505
+ except (TypeError, ValueError):
506
+ # MetadataValidation already reports a non-numeric age; dropping
507
+ # it here keeps a bad cell from failing the upload as well.
508
+ pass
509
+
510
+ return supplied
511
+
475
512
  def generate_payload(self) -> Dict[str, Any]:
476
513
  payload = {
477
514
  "study_id": self.row.study_id,
478
515
  "site_id": self.row.site_id,
479
516
  "rater_id": self.row.rater_id,
480
517
  "participant_id": self.row.participant_id,
481
- "age": self.row.age,
482
- "sex": self.row.sex,
483
- "race": self.row.race,
484
518
  "language": self.row.language,
485
519
  "visit_name": self.row.visit_name,
486
520
  "visit_order": int(self.row.visit_order),
@@ -493,6 +527,7 @@ class UploadUtils:
493
527
  "is_last_recording": self.row.is_last_recording,
494
528
  # "time_collected": parser.parse(self.row.time_collected).isoformat(),
495
529
  }
530
+ payload.update(self.demographic_fields())
496
531
  return payload
497
532
 
498
533
  def generate_processed_payload(
@@ -518,6 +553,7 @@ class UploadUtils:
518
553
  site_country = getattr(self.row, "site_country", None)
519
554
  if site_country:
520
555
  payload["site_country"] = site_country
556
+ payload.update(self.demographic_fields())
521
557
  return payload
522
558
 
523
559
  def post(
@@ -757,7 +793,8 @@ class ProcessedMetadataValidation:
757
793
  orig_bucket, orig_key = _get_bucket_n_key_path_from_s3url(
758
794
  grp.iloc[0]["recording"]
759
795
  )
760
- merged_key = os.path.join(os.path.dirname(orig_key), merged_name)
796
+ # S3 keys always use "/" — os.path.join would insert "\" on Windows.
797
+ merged_key = posixpath.join(posixpath.dirname(orig_key), merged_name)
761
798
 
762
799
  # Create new URI (no upload, logical merge only)
763
800
  new_uri = f"s3://{orig_bucket}/{merged_key}"
@@ -810,9 +847,11 @@ def get_last_n_directories(filepath, n=3):
810
847
  result = None
811
848
  error = None
812
849
  try:
813
- parts = filepath.split(os.sep)
850
+ # Normalize Windows backslashes so the result is always a
851
+ # forward-slash S3 key regardless of the local OS.
852
+ parts = filepath.replace("\\", "/").split("/")
814
853
  last_parts = parts[-(n + 1) :]
815
- result = os.sep.join(last_parts)
854
+ result = "/".join(last_parts)
816
855
  except Exception as e:
817
856
  error = "Could not extract container name and container version from file path"
818
857
  return result, error
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
2
- Name: willisapi-client
3
- Version: 1.9.8
1
+ Metadata-Version: 2.4
2
+ Name: willisapi_client
3
+ Version: 2.0
4
4
  Summary: A Python client for willisapi
5
5
  Home-page: https://github.com/bklynhlth/willsiapi_client
6
6
  Author: bklynhlth
@@ -9,6 +9,39 @@ License: Apache
9
9
  Requires-Python: >=3.9
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
+ Requires-Dist: aiohttp==3.9.0
13
+ Requires-Dist: aiosignal==1.3.1
14
+ Requires-Dist: async-timeout==4.0.2
15
+ Requires-Dist: asyncio==3.4.3
16
+ Requires-Dist: attrs==23.1.0
17
+ Requires-Dist: certifi==2023.7.22
18
+ Requires-Dist: charset-normalizer==3.2.0
19
+ Requires-Dist: frozenlist==1.4.0
20
+ Requires-Dist: idna==3.4
21
+ Requires-Dist: multidict==6.0.4
22
+ Requires-Dist: nest-asyncio==1.5.7
23
+ Requires-Dist: numpy==1.25.2
24
+ Requires-Dist: pandas==2.0.3
25
+ Requires-Dist: pytest==7.4.0
26
+ Requires-Dist: pytest-cov==4.1.0
27
+ Requires-Dist: python-dateutil==2.8.2
28
+ Requires-Dist: pytz==2023.3
29
+ Requires-Dist: requests==2.31.0
30
+ Requires-Dist: six==1.16.0
31
+ Requires-Dist: tqdm==4.66.1
32
+ Requires-Dist: tzdata==2023.3
33
+ Requires-Dist: urllib3==2.0.7
34
+ Requires-Dist: yarl==1.9.2
35
+ Dynamic: author
36
+ Dynamic: author-email
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: license
41
+ Dynamic: license-file
42
+ Dynamic: requires-dist
43
+ Dynamic: requires-python
44
+ Dynamic: summary
12
45
 
13
46
  <!-- Output copied to clipboard! -->
14
47
 
File without changes