seer-pas-sdk 1.1.1__tar.gz → 1.2.1__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 (40) hide show
  1. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/PKG-INFO +1 -1
  2. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/docs/index.qmd +57 -30
  3. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/common/__init__.py +53 -6
  4. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/core/sdk.py +244 -194
  5. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/core/unsupported.py +295 -195
  6. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk.egg-info/PKG-INFO +1 -1
  7. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/.github/workflows/lint.yml +0 -0
  8. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/.github/workflows/publish.yml +0 -0
  9. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/.github/workflows/test.yml +0 -0
  10. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/.gitignore +0 -0
  11. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/.pre-commit-config.yaml +0 -0
  12. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/LICENSE.txt +0 -0
  13. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/README.md +0 -0
  14. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/docs/_quarto.yml +0 -0
  15. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/pyproject.toml +0 -0
  16. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/__init__.py +0 -0
  17. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/auth/__init__.py +0 -0
  18. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/auth/auth.py +0 -0
  19. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/common/errors.py +0 -0
  20. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/common/groupanalysis.py +0 -0
  21. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/core/__init__.py +0 -0
  22. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/objects/__init__.py +0 -0
  23. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/objects/groupanalysis.py +0 -0
  24. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/objects/headers.py +0 -0
  25. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/objects/platemap.py +0 -0
  26. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk/objects/volcanoplot.py +0 -0
  27. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk.egg-info/SOURCES.txt +0 -0
  28. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk.egg-info/dependency_links.txt +0 -0
  29. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk.egg-info/requires.txt +0 -0
  30. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/seer_pas_sdk.egg-info/top_level.txt +0 -0
  31. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/setup.cfg +0 -0
  32. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/__init__.py +0 -0
  33. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/conftest.py +0 -0
  34. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/objects/__init__.py +0 -0
  35. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/objects/test_platemap.py +0 -0
  36. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/test_auth.py +0 -0
  37. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/test_common.py +0 -0
  38. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/test_objects.py +0 -0
  39. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/test_sdk.py +0 -0
  40. {seer_pas_sdk-1.1.1 → seer_pas_sdk-1.2.1}/tests/unsupported_platemap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seer-pas-sdk
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: SDK for Seer Proteograph Analysis Suite (PAS)
5
5
  Author-email: Ryan Sun <rsun@seer.bio>
6
6
  License:
@@ -18,12 +18,27 @@ $ pip install seer-pas-sdk
18
18
  This page gives an overview of the SDK's feature. Complete documentation for each class / method can be found [here](reference/).
19
19
 
20
20
  ### Configuration
21
- PAS has a simple authorization system that just involves your username and password fields like on the web app. You can define your username and password for your own ready reference and convenience as follows:
21
+ The PAS SDK has a simple authorization system that involves your username and password fields like on the web app. You can define your username and password for your own ready reference and convenience as follows:
22
22
  ```{python}
23
23
  USERNAME = "gnu403"
24
24
  PASSWORD = "Test!234567"
25
25
  ```
26
26
 
27
+ The PAS SDK requires either a `tenant` or `tenant_id` argument in the SDK object constructor.
28
+
29
+ `tenant` refers to the user provided name of the tenant.
30
+
31
+ `tenant_id` refers to the immutable and unique identifier of the tenant.
32
+ `tenant_id` is an absolute reference to the tenant, even if the tenant name is changed.
33
+
34
+ More details on multi-tenant management can be found in the [Multi Tenant Management](#multi-tenant-management) section below.
35
+
36
+ You can define your tenant name or tenant ID as follows:
37
+ ```{python}
38
+ TENANT = "My Tenant Name"
39
+ TENANT_ID = "abc1234abc1234"
40
+ ```
41
+
27
42
  You may also choose to pass in an `instance` param in the SDK object to instantiate the PAS SDK to the EU or US instance.:
28
43
  ```{python}
29
44
  INSTANCE = "US"
@@ -38,10 +53,13 @@ After importing the SeerSDK module, you can instantiate an object in the followi
38
53
  from seer_pas_sdk import SeerSDK
39
54
 
40
55
  # Instantiate an SDK object with your credentials:
41
- sdk = SeerSDK(USERNAME, PASSWORD)
56
+ sdk = SeerSDK(USERNAME, PASSWORD, tenant=TENANT)
42
57
 
43
- # You could alternatively pass your credentials and/or the instance directly into the instantiated object.
44
- sdk = SeerSDK(USERNAME, PASSWORD, INSTANCE)
58
+ # Instantiate an SDK object with your credentials and instance:
59
+ sdk = SeerSDK(USERNAME, PASSWORD, INSTANCE, tenant=TENANT)
60
+
61
+ # Instantiate an SDK object with your credentials and tenant ID:
62
+ sdk = SeerSDK(USERNAME, PASSWORD, INSTANCE, tenant_id=TENANT_ID)
45
63
  ```
46
64
 
47
65
  ```{python}
@@ -56,18 +74,16 @@ Additional information and examples can also be found below.
56
74
  ### Multi Tenant Management
57
75
  Introduced in version 0.2.0
58
76
 
59
- By default, you will be active in your home tenant upon log in. The home tenant is defined as the organization account that issued the original invitation for the user to join PAS.
60
- The optional 'tenant' parameter is available in the SeerSDK constructor to navigate directly to a desired tenant.
61
- A notification message will display upon login.
62
-
63
-
64
77
  The following tools are available to navigate between tenants:
65
78
  ```{python}
66
79
  #| eval: false
67
80
  from seer_pas_sdk import SeerSDK
68
81
 
82
+ # Assume tenant upon login
69
83
  sdk = SeerSDK(USERNAME, PASSWORD, INSTANCE, tenant='My Active Tenant')
70
84
 
85
+ sdk = SeerSDK(USERNAME, PASSWORD, INSTANCE, tenant_id='myuuidstring-1234')
86
+
71
87
  # Retrieve value of current active tenant
72
88
  print(sdk.get_active_tenant())
73
89
 
@@ -337,7 +353,7 @@ example = sdk.find_msruns(sample_ids)
337
353
  log(example)
338
354
  ```
339
355
  ```
340
- [{'id': '81c6a180-15e0-11ee-bdf1-bbaa73585acf', 'sample_id': '812139c0-15e0-11ee-bdf1-bbaa73585acf', 'raw_file_path': '7ec8cad0-15e0-11ee-bdf1-bbaa73585acf/20230628182044224/TestFile2.raw', 'well_location': 'D11', 'nanoparticle': '', 'instrument_name': '', 'created_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'created_timestamp': '2023-06-28T18:20:49.006Z', 'last_modified_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'last_modified_timestamp': '2023-06-28T18:20:49.006Z', 'user_group': None, 'sample_id_tracking': 'A112', 'nanoparticle_id': '', 'control': '', 'control_id': '', 'date_sample_prep': '', 'sample_volume': '', 'peptide_concentration': '', 'peptide_mass_sample': '', 'dilution_factor': '', 'kit_id': None, 'injection_timestamp': None, 'ms_instrument_sn': None, 'recon_volume': None, 'gradient': None}, {'id': '816a9ed0-15e0-11ee-bdf1-bbaa73585acf', 'sample_id': '803e05b0-15e0-11ee-bdf1-bbaa73585acf', 'raw_file_path': '7ec8cad0-15e0-11ee-bdf1-bbaa73585acf/20230628182044224/TestFile1.raw', 'well_location': 'C11', 'nanoparticle': 'NONE', 'instrument_name': '', 'created_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'created_timestamp': '2023-06-28T18:20:48.408Z', 'last_modified_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'last_modified_timestamp': '2023-06-28T18:20:48.408Z', 'user_group': None, 'sample_id_tracking': 'A111', 'nanoparticle_id': 'NONE', 'control': 'MPE Control', 'control_id': 'MPE Control', 'date_sample_prep': '', 'sample_volume': '20.0', 'peptide_concentration': '59.514', 'peptide_mass_sample': '8.57', 'dilution_factor': '1.0', 'kit_id': None, 'injection_timestamp': None, 'ms_instrument_sn': None, 'recon_volume': None, 'gradient': None}]
356
+ [{'id': '81c6a180-15e0-11ee-bdf1-bbaa73585acf', 'sample_uuid': '812139c0-15e0-11ee-bdf1-bbaa73585acf', 'raw_file_path': '7ec8cad0-15e0-11ee-bdf1-bbaa73585acf/20230628182044224/TestFile2.raw', 'well_location': 'D11', 'nanoparticle': '', 'instrument_name': '', 'created_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'created_timestamp': '2023-06-28T18:20:49.006Z', 'last_modified_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'last_modified_timestamp': '2023-06-28T18:20:49.006Z', 'user_group': None, 'sample_id': 'A112', 'nanoparticle_id': '', 'control': '', 'control_id': '', 'date_sample_prep': '', 'sample_volume': '', 'peptide_concentration': '', 'peptide_mass_sample': '', 'dilution_factor': '', 'kit_id': None, 'injection_timestamp': None, 'ms_instrument_sn': None, 'recon_volume': None, 'gradient': None}, {'id': '816a9ed0-15e0-11ee-bdf1-bbaa73585acf', 'sample_uuid': '803e05b0-15e0-11ee-bdf1-bbaa73585acf', 'raw_file_path': '7ec8cad0-15e0-11ee-bdf1-bbaa73585acf/20230628182044224/TestFile1.raw', 'well_location': 'C11', 'nanoparticle': 'NONE', 'instrument_name': '', 'created_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'created_timestamp': '2023-06-28T18:20:48.408Z', 'last_modified_by': '04936dea-d255-4130-8e82-2f28938a8f9a', 'last_modified_timestamp': '2023-06-28T18:20:48.408Z', 'user_group': None, 'sample_id': 'A111', 'nanoparticle_id': 'NONE', 'control': 'MPE Control', 'control_id': 'MPE Control', 'date_sample_prep': '', 'sample_volume': '20.0', 'peptide_concentration': '59.514', 'peptide_mass_sample': '8.57', 'dilution_factor': '1.0', 'kit_id': None, 'injection_timestamp': None, 'ms_instrument_sn': None, 'recon_volume': None, 'gradient': None}]
341
357
  ```
342
358
 
343
359
  There is also an option to return everything as a DataFrame instead:
@@ -347,7 +363,7 @@ example = sdk.find_msruns(sample_ids, as_df=True)
347
363
  log(example)
348
364
  ```
349
365
  ```
350
- id sample_id raw_file_path well_location nanoparticle instrument_name created_by created_timestamp last_modified_by last_modified_timestamp space sample_id_tracking nanoparticle_id control control_id date_sample_prep sample_volume peptide_concentration peptide_mass_sample dilution_factor kit_id injection_timestamp ms_instrument_sn recon_volume gradient
366
+ id sample_uuid raw_file_path well_location nanoparticle instrument_name created_by created_timestamp last_modified_by last_modified_timestamp space sample_id nanoparticle_id control control_id date_sample_prep sample_volume peptide_concentration peptide_mass_sample dilution_factor kit_id injection_timestamp ms_instrument_sn recon_volume gradient
351
367
  0 81c6a180-15e0-11ee-bdf1-bbaa73585acf 812139c0-15e0-11ee-bdf1-bbaa73585acf 7ec8cad0-15e0-11ee-bdf1-bbaa73585acf/202306281... D11 04936dea-d255-4130-8e82-2f28938a8f9a 2023-06-28T18:20:49.006Z 04936dea-d255-4130-8e82-2f28938a8f9a 2023-06-28T18:20:49.006Z None A112 None None None None None
352
368
  1 816a9ed0-15e0-11ee-bdf1-bbaa73585acf 803e05b0-15e0-11ee-bdf1-bbaa73585acf 7ec8cad0-15e0-11ee-bdf1-bbaa73585acf/202306281... C11 NONE 04936dea-d255-4130-8e82-2f28938a8f9a 2023-06-28T18:20:48.408Z 04936dea-d255-4130-8e82-2f28938a8f9a 2023-06-28T18:20:48.408Z None A111 NONE MPE Control MPE Control 20.0 59.514 8.57 1.0 None None None None None
353
369
  ```
@@ -578,10 +594,17 @@ log(analysis)
578
594
 
579
595
 
580
596
  ### Find Analyses
581
- Returns a list of analyses objects for the authenticated user. If no `analysis_id` is provided, returns all analyses for the authenticated user.
597
+ Returns a list of analyses objects for the authenticated user. If `None` is provided for all query arguments, returns all analyses available to the user within the active tenant.
582
598
 
583
599
  ###### <u>Params</u>
584
- `analysis_id`: (`str`, optional) Unique ID of the analysis to be fetched, defaulted to None.
600
+ * `analysis_id`: (`str`, optional) Unique ID of the analysis to be fetched, defaulted to None.
601
+ * `analysis_name`: (`str`, optional) Name of the analysis to be fetched, defaulted to None. Results will be matched on a substring basis.
602
+ * `folder_id`: (`str`, optional) Unique ID of the folder to fetch analyses from, defaulted to None.
603
+ * `folder_name`: (`str`, optional) Name of the folder to fetch analyses from, defaulted to None.
604
+ * `project_id`: (`str`, optional) Unique ID of the project to filter the result set of analyses, defaulted to None.
605
+ * `project_name`: (`str`, optional) Name of the project to filter the result set of analyses, defaulted to None.
606
+ * `plate_name`: (`str`, optional) Name of a plate to filter the result set of analyses, defaulted to None.
607
+ * `as_df`: (`bool`, optional) Whether the result should be converted to a DataFrame, defaulted to False.
585
608
  <br>
586
609
 
587
610
  ###### <u>Returns</u>
@@ -955,11 +978,9 @@ log(sdk.group_analysis_results(group_analysis_id, box_plot_info))
955
978
  Downloads the FASTA file(s) associated with an analysis protocol. You can specify an analysis_id (the function will resolve the protocol automatically) or provide an analysis_protocol_id directly.
956
979
 
957
980
  ###### <u>Params</u>
958
- * `analysis_protocol_id`: (`str`, optional) ID of the analysis protocol whose FASTA file(s) you want.
959
-
960
- * `analysis_id`: (`str`, optional) ID of the analysis whose protocol FASTA file(s) you want.
961
-
962
- * `download_path`: (`str`, optional) Directory to save files to. Defaults to the current working directory.
981
+ * `analysis_protocol_id`: (`str`, optional) The unique ID of the analysis protocol associated with the FASTA files to download.
982
+ * `analysis_id`: (`str`, optional) The unique ID of the analysis whose protocol FASTA file(s) will be downloaded.
983
+ * `analysis_name`: (`str`, optional) The name of the analysis whose protocol FASTA file(s) will be downloaded.
963
984
 
964
985
  Note: Provide either analysis_id or analysis_protocol_id (but not both).
965
986
 
@@ -977,6 +998,10 @@ sdk.download_analysis_protocol_fasta(
977
998
  )
978
999
  ```
979
1000
 
1001
+ ```
1002
+ ['./uniprot_human_2023_08.fasta', './contaminants.fasta']
1003
+ ```
1004
+
980
1005
  Download by analysis protocol ID to a specific folder:
981
1006
  ```{python}
982
1007
  #| eval: false
@@ -991,16 +1016,20 @@ sdk.download_analysis_protocol_fasta(
991
1016
  ```
992
1017
  <br>
993
1018
 
994
- ### Get Analysis Protocol FASTA link
995
- Returns signed download links for the FASTA file(s) associated with an analysis protocol. You can specify an analysis_id (the function will resolve the protocol automatically) or provide an analysis_protocol_id directly.
1019
+ ### Get Analysis Protocol FASTA URLs
1020
+ Returns download URLs for the FASTA file(s) associated with an analysis protocol. You can specify an analysis_id (the function will resolve the protocol automatically) or provide an analysis_protocol_id directly.
1021
+
1022
+ Download URLs are valid for 15 minutes after generation.
996
1023
 
997
1024
  ###### <u>Params</u>
998
- * `analysis_protocol_id`: (`str`, optional) ID of the analysis protocol whose FASTA file(s) you want.
999
- * `analysis_id`: (`str`, optional) ID of the analysis whose protocol FASTA file(s) you want.
1000
- Note: Provide either analysis_id or analysis_protocol_id (but not both).
1025
+ * `analysis_protocol_id`: (`str`, optional) The unique ID of the analysis protocol associated with the FASTA files.
1026
+ * `analysis_id`: (`str`, optional) The unique ID of the analysis whose protocol FASTA file(s) should be retrieved.
1027
+ * `analysis_name`: (`str`, optional) The name of the analysis whose protocol FASTA file(s) should be retrieved.
1028
+
1029
+ If both parameters are provided, `analysis_protocol_id` takes precedence.
1001
1030
 
1002
1031
  ###### <u>Returns</u>
1003
- * links: (`list[dict]`) List of dictionaries containing filename and signed URL for each FASTA file.
1032
+ * links: (`dict`) Dictionary containing filename and signed URL as key-value pairs for the FASTA files linked to the protocol.
1004
1033
 
1005
1034
  ###### <u>Examples</u>
1006
1035
  Get by analysis ID:
@@ -1012,10 +1041,8 @@ sdk.get_analysis_protocol_fasta_link(
1012
1041
  ```
1013
1042
 
1014
1043
  ```
1015
- [
1016
- {"filename": "uniprot_human_2023_08.fasta", "url": "https://...signed..."},
1017
- {"filename": "contaminants.fasta", "url": "https://...signed..."}
1018
- ]
1044
+ {"uniprot_human_2023_08.fasta" : "https://...signed...",
1045
+ "contaminants.fasta" : "https://...signed..."}
1019
1046
  ```
1020
1047
  Get by analysis protocol ID:
1021
1048
  ```{python}
@@ -1026,8 +1053,8 @@ sdk.get_analysis_protocol_fasta_link(
1026
1053
  ```
1027
1054
  ```
1028
1055
  [
1029
- {"filename": "uniprot_human_2023_08.fasta", "url": "https://...signed..."},
1030
- {"filename": "contaminants.fasta", "url": "https://...signed..."}
1056
+ {"uniprot_human_2023_08.fasta" : "https://...signed...",
1057
+ "contaminants.fasta" : "https://...signed..."}
1031
1058
  ]
1032
1059
  ```
1033
1060
  <hr>
@@ -99,7 +99,7 @@ def dict_to_df(data):
99
99
 
100
100
 
101
101
  # Most cases appear to be a .tsv file.
102
- def download_df(url, is_tsv=True, dtype={}):
102
+ def download_df(url, is_tsv=True, dtype={}, usecols=None):
103
103
  """
104
104
  Fetches a TSV/CSV file from a URL and returns as a Pandas DataFrame.
105
105
 
@@ -114,6 +114,9 @@ def download_df(url, is_tsv=True, dtype={}):
114
114
  dtype : dict
115
115
  Data type conversion when intaking columns. e.g. {'a': str, 'b': np.float64}
116
116
 
117
+ usecols : list
118
+ Subset of columns to download. If not specified, downloads all columns.
119
+
117
120
  Returns
118
121
  -------
119
122
  pandas.core.frame.DataFrame
@@ -139,11 +142,9 @@ def download_df(url, is_tsv=True, dtype={}):
139
142
 
140
143
  if not url:
141
144
  return pd.DataFrame()
142
- url_content = io.StringIO(requests.get(url).content.decode("utf-8"))
143
- if is_tsv:
144
- csv = pd.read_csv(url_content, sep="\t", dtype=dtype)
145
- else:
146
- csv = pd.read_csv(url_content, dtype=dtype)
145
+ csv = pd.read_csv(
146
+ url, dtype=dtype, sep="\t" if is_tsv else ",", usecols=usecols
147
+ )
147
148
  return csv
148
149
 
149
150
 
@@ -679,6 +680,52 @@ def camel_case(s):
679
680
  return "".join([s[0].lower(), s[1:]])
680
681
 
681
682
 
683
+ def validate_d_zip_file(file):
684
+ """
685
+ Return True if a .d.zip file aligns with Seer requirements for PAS upload.
686
+
687
+ Parameters
688
+ ----------
689
+ file : str
690
+ The name of the zip file.
691
+
692
+ Returns
693
+ -------
694
+ bool
695
+ True if the .d.zip file is valid, False otherwise.
696
+ """
697
+
698
+ if not file.lower().endswith(".d.zip"):
699
+ return False
700
+
701
+ basename = os.path.basename(file)
702
+
703
+ # Remove the .zip extension to get the .d folder name
704
+ d_name = basename[:-4]
705
+
706
+ try:
707
+ with zipfile.ZipFile(file, "r") as zf:
708
+ names = zf.namelist()
709
+
710
+ except:
711
+ return False
712
+
713
+ if not names:
714
+ return False
715
+
716
+ # check for files at the root level
717
+ root_entries = [n for n in names if "/" not in n.rstrip("/")]
718
+ if root_entries:
719
+ return False
720
+
721
+ # find folders
722
+ top_level = {n.split("/")[0] for n in names}
723
+ if len(top_level) != 1 or d_name not in top_level:
724
+ return False
725
+
726
+ return True
727
+
728
+
682
729
  def rename_d_zip_file(source, destination):
683
730
  """
684
731
  Renames a .d.zip file. The function extracts the contents of the source zip file, renames the inner .d folder, and rezips the contents into the destination zip file.