pynmrstar 3.3.6__pp311-pypy311_pp73-win_amd64.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 pynmrstar might be problematic. Click here for more details.

Binary file
pynmrstar/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """This module provides :py:class:`pynmrstar.Entry`, :py:class:`pynmrstar.Saveframe`,
4
+ :py:class:`pynmrstar.Loop`, and :py:class:`pynmrstar.Schema` objects.
5
+
6
+ It also provides some utility functions in :py:obj:`pynmrstar.utils`
7
+
8
+ Use python's built in help function for documentation."""
9
+
10
+ import decimal as _decimal
11
+ import logging
12
+ import os
13
+
14
+ try:
15
+ import cnmrstar
16
+ except ImportError:
17
+ try:
18
+ import pynmrstar.cnmrstar as cnmrstar
19
+ except ImportError:
20
+ if os.environ.get('READTHEDOCS'):
21
+ cnmrstar = None
22
+ else:
23
+ raise ImportError('Could not import cnmrstar sub-module! Your installation appears to be broken.')
24
+
25
+ from pynmrstar import utils
26
+ from pynmrstar._internal import __version__, min_cnmrstar_version
27
+ from pynmrstar.entry import Entry
28
+ from pynmrstar.loop import Loop
29
+ from pynmrstar.parser import Parser as _Parser
30
+ from pynmrstar.saveframe import Saveframe
31
+ from pynmrstar.schema import Schema
32
+ import pynmrstar.definitions as definitions
33
+
34
+ if cnmrstar:
35
+ if "version" not in dir(cnmrstar):
36
+ raise ImportError(f"Could not determine the version of cnmrstar installed, and version {min_cnmrstar_version} or "
37
+ "greater is required.")
38
+ if cnmrstar.version() < min_cnmrstar_version:
39
+ raise ImportError("The version of the cnmrstar module installed does not meet the requirements. As this should be "
40
+ f"handled automatically, there may be an issue with your installation. Version installed: "
41
+ f"{cnmrstar.version()}. Version required: {min_cnmrstar_version}")
42
+
43
+ # Set up logging
44
+ logger = logging.getLogger('pynmrstar')
45
+
46
+ # This makes sure that when decimals are printed a lower case "e" is used
47
+ _decimal.getcontext().capitals = 0
48
+
49
+ del loop
50
+ del entry
51
+ del saveframe
52
+ del schema
53
+ del parser
54
+
55
+ __all__ = ['Loop', 'Saveframe', 'Entry', 'Schema', 'definitions', 'utils', '__version__', 'exceptions', 'cnmrstar']
pynmrstar/_internal.py ADDED
@@ -0,0 +1,261 @@
1
+ import decimal
2
+ import json
3
+ import logging
4
+ import os
5
+ import time
6
+ import zlib
7
+ from datetime import date
8
+ from gzip import GzipFile
9
+ from io import StringIO, BytesIO
10
+ from pathlib import Path
11
+ from typing import Dict, Union, IO, List, Tuple
12
+ from urllib.error import URLError
13
+
14
+ import requests
15
+
16
+ import pynmrstar
17
+
18
+ __version__: str = "3.3.6"
19
+ min_cnmrstar_version: str = "3.2.0"
20
+
21
+ # Create a session to reuse for the duration of the program run
22
+ _session = requests.session()
23
+
24
+ logger = logging.getLogger('pynmrstar')
25
+
26
+
27
+ # noinspection PyDefaultArgument
28
+ def _get_comments(_comment_cache: Dict[str, Dict[str, str]] = {}) -> Dict[str, Dict[str, str]]:
29
+ """ Loads the comments that should be placed in written files.
30
+
31
+ The default argument is mutable on purpose, as it is used as a cache for memoization."""
32
+
33
+ # Comment dictionary already exists
34
+ if _comment_cache:
35
+ return _comment_cache
36
+
37
+ file_to_load = os.path.join(os.path.dirname(os.path.realpath(__file__)))
38
+ file_to_load = os.path.join(file_to_load, "reference_files/comments.str")
39
+
40
+ # The import needs to be here to avoid import errors due to circular imports
41
+ from pynmrstar.entry import Entry
42
+ try:
43
+ comment_entry = Entry.from_file(file_to_load)
44
+ except IOError:
45
+ # Load the comments from Github if we can't find them locally
46
+ try:
47
+ logger.warning('Could not load comments from disk. Loading from web...')
48
+ comment_entry = Entry.from_file(_interpret_file(pynmrstar.definitions.COMMENT_URL))
49
+ except Exception:
50
+ logger.exception('Could not load comments from web. No comments will be shown.')
51
+ # No comments will be printed
52
+ return {}
53
+
54
+ # Load the comments
55
+ comment_records = comment_entry[0][0].get_tag(["category", "comment", "every_flag"])
56
+ comment_map = {'N': False, 'Y': True}
57
+ for comment in comment_records:
58
+ if comment[1] != ".":
59
+ _comment_cache[comment[0]] = {'comment': comment[1].rstrip() + "\n\n",
60
+ 'every_flag': comment_map[comment[2]]}
61
+
62
+ return _comment_cache
63
+
64
+
65
+ def _json_serialize(obj: object) -> str:
66
+ """JSON serializer for objects not serializable by default json code"""
67
+
68
+ # Serialize datetime.date objects by calling str() on them
69
+ if isinstance(obj, (date, decimal.Decimal)):
70
+ return str(obj)
71
+ raise TypeError("Type not serializable: %s" % type(obj))
72
+
73
+
74
+ def _get_url_reliably(url: str, wait_time: float = 10, raw: bool = False, timeout: int = 10, retries: int = 2):
75
+ """ Attempts to load data from a URL, retrying the specified number of times with an exponential
76
+ backoff if rate limited. Fails immediately on 4xx errors that are not 403."""
77
+
78
+ global _session
79
+
80
+ try:
81
+ response = _session.get(url, timeout=timeout,
82
+ headers={'Application': f'PyNMRSTAR {__version__}'})
83
+ except requests.exceptions.ConnectionError:
84
+ _session = requests.session()
85
+ try:
86
+ response = _session.get(url, timeout=timeout,
87
+ headers={'Application': f'PyNMRSTAR {__version__}'})
88
+ except requests.exceptions.ConnectionError:
89
+ raise requests.exceptions.HTTPError("A ConnectionError was thrown during an attempt to load the entry.")
90
+
91
+ # We are rate limited - sleep and try again
92
+ if response.status_code == 403:
93
+ if retries > 0:
94
+ logger.warning(f'We were rate limited. Sleeping for {wait_time} seconds.')
95
+ time.sleep(wait_time)
96
+ return _get_url_reliably(url, wait_time=wait_time * 2, raw=raw, timeout=timeout,
97
+ retries=retries - 1)
98
+ else:
99
+ raise requests.exceptions.HTTPError("Continued to receive 403 (forbidden, due to rate limit) after multiple wait times.") \
100
+ from None
101
+ if response.status_code == 404:
102
+ raise KeyError(f"Server returned 404.") from None
103
+ response.raise_for_status()
104
+ if raw:
105
+ return response.content
106
+ else:
107
+ return response.text
108
+
109
+
110
+ def _get_entry_from_database(entry_num: Union[str, int],
111
+ convert_data_types: bool = False,
112
+ schema: 'pynmrstar.Schema' = None) -> 'pynmrstar.Entry':
113
+ """ Fetches an entry from the API (or falls back to the FTP site) in
114
+ as reliable and robust a way as possible. Used by Entry.from_database(). """
115
+
116
+ entry_num = str(entry_num).lower()
117
+ if entry_num.startswith("bmr"):
118
+ entry_num = entry_num[3:]
119
+
120
+ # Try to load the entry using JSON
121
+
122
+ entry_url: str = (pynmrstar.definitions.API_URL + "/entry/%s?format=zlib") % entry_num
123
+
124
+ try:
125
+ serialized_ent = _get_url_reliably(entry_url, raw=True, retries=2)
126
+ json_data = json.loads(zlib.decompress(serialized_ent).decode())
127
+ if "error" in json_data:
128
+ raise RuntimeError('Something wrong with API response.')
129
+ ent = pynmrstar.Entry.from_json(json_data)
130
+ except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError, RuntimeError):
131
+ # Can't fall back to FTP for chemcomps
132
+ if entry_num.startswith("chemcomp"):
133
+ raise IOError("Unable to load that chemcomp from the API.")
134
+
135
+ # We're going to try again from the FTP
136
+ logger.warning('Failed to download entry from the API, trying again from the FTP site.')
137
+ if "bmse" in entry_num or "bmst" in entry_num:
138
+ url = f"{pynmrstar.definitions.FTP_URL}/metabolomics/entry_directories/{entry_num}/{entry_num}.str"
139
+ else:
140
+ url = f"{pynmrstar.definitions.FTP_URL}/entry_directories/bmr{entry_num}/bmr{entry_num}_3.str"
141
+ try:
142
+ # Use a longer timeout for the timeout
143
+ entry_content = _get_url_reliably(url, raw=False, timeout=20, retries=1)
144
+ ent = pynmrstar.Entry.from_string(entry_content)
145
+ except requests.exceptions.HTTPError:
146
+ raise IOError(f"Entry {entry_num} does not exist in the public database.") from None
147
+ except URLError:
148
+ raise IOError("You don't appear to have an active internet connection. Cannot fetch entry.") from None
149
+
150
+ except KeyError:
151
+ raise IOError(f"Entry {entry_num} does not exist in the public database.") from None
152
+
153
+ # Update the entry source
154
+ ent.source = f"from_database({entry_num})"
155
+ for each_saveframe in ent:
156
+ each_saveframe.source = ent.source
157
+ for each_loop in each_saveframe:
158
+ each_loop.source = ent.source
159
+
160
+ if convert_data_types:
161
+ schema = pynmrstar.utils.get_schema(schema)
162
+ for each_saveframe in ent:
163
+ for tag in each_saveframe.tags:
164
+ cur_tag = each_saveframe.tag_prefix + "." + tag[0]
165
+ tag[1] = schema.convert_tag(cur_tag, tag[1])
166
+ for loop in each_saveframe:
167
+ for row in loop.data:
168
+ for pos in range(0, len(row)):
169
+ category = loop.category + "." + loop.tags[pos]
170
+ row[pos] = schema.convert_tag(category, row[pos])
171
+
172
+ return ent
173
+
174
+
175
+ def _interpret_file(the_file: Union[str, Path, IO]) -> StringIO:
176
+ """Helper method returns some sort of object with a read() method.
177
+ the_file could be a URL, a file location, a file object, or a
178
+ gzipped version of any of the above."""
179
+
180
+ if hasattr(the_file, 'read'):
181
+ read_data: Union[bytes, str] = the_file.read()
182
+ if type(read_data) == bytes:
183
+ buffer: BytesIO = BytesIO(read_data)
184
+ elif type(read_data) == str:
185
+ buffer = BytesIO(read_data.encode())
186
+ else:
187
+ raise IOError("What did your file object return when .read() was called on it?")
188
+ elif isinstance(the_file, str):
189
+ if the_file.startswith("http://") or the_file.startswith("https://") or the_file.startswith("ftp://"):
190
+ buffer = BytesIO(_get_url_reliably(the_file, raw=True, retries=0))
191
+ else:
192
+ with open(the_file, 'rb') as read_file:
193
+ buffer = BytesIO(read_file.read())
194
+ elif isinstance(the_file, Path):
195
+ with open(str(the_file), 'rb') as read_file:
196
+ buffer = BytesIO(read_file.read())
197
+ else:
198
+ raise ValueError("Cannot figure out how to interpret the file you passed.")
199
+
200
+ # Decompress the buffer if we are looking at a gzipped file
201
+ try:
202
+ gzip_buffer = GzipFile(fileobj=buffer)
203
+ gzip_buffer.readline()
204
+ gzip_buffer.seek(0)
205
+ buffer = BytesIO(gzip_buffer.read())
206
+ # Apparently we are not looking at a gzipped file
207
+ except (IOError, AttributeError, UnicodeDecodeError):
208
+ pass
209
+
210
+ buffer.seek(0)
211
+ return StringIO(buffer.read().decode().replace("\r\n", "\n").replace("\r", "\n"))
212
+
213
+
214
+ def get_clean_tag_list(item: Union[str, List[str], Tuple[str]]) -> List[Dict[str, str]]:
215
+ """ Converts the provided item to a list of dictionaries of
216
+ {
217
+ formatted -> just the lower case tag name (category stripped)
218
+ original -> whatever was provided, completely unmodified
219
+ }"""
220
+
221
+ if not isinstance(item, (str, list, tuple)):
222
+ raise ValueError('Invalid object provided. Only a tag name (str), or list of tags (list or tuple)'
223
+ ' are valid inputs to this function.')
224
+
225
+ if isinstance(item, list):
226
+ tag_list: List[str] = item
227
+ elif isinstance(item, tuple):
228
+ tag_list = list(item)
229
+ elif isinstance(item, str):
230
+ tag_list = [item]
231
+ else:
232
+ raise ValueError(f'The value you provided was not a string, list, or tuple. Item: {repr(item)}')
233
+
234
+ try:
235
+ return [{"formatted": pynmrstar.utils.format_tag_lc(_), "original": _} for _ in tag_list]
236
+ except AttributeError:
237
+ raise ValueError('Your list or tuple may only contain tag names expressed as strings.')
238
+
239
+
240
+ def write_to_file(nmrstar_object: Union['pynmrstar.Entry', 'pynmrstar.Saveframe'],
241
+ file_name: Union[str, Path],
242
+ format_: str = "nmrstar",
243
+ show_comments: bool = True,
244
+ skip_empty_loops: bool = False,
245
+ skip_empty_tags: bool = False):
246
+ """ Writes the object to the specified file in NMR-STAR format. """
247
+
248
+ if format_ not in ["nmrstar", "json"]:
249
+ raise ValueError("Invalid output format.")
250
+
251
+ data_to_write = ''
252
+ if format_ == "nmrstar":
253
+ data_to_write = nmrstar_object.format(show_comments=show_comments,
254
+ skip_empty_loops=skip_empty_loops,
255
+ skip_empty_tags=skip_empty_tags)
256
+ elif format_ == "json":
257
+ data_to_write = nmrstar_object.get_json()
258
+
259
+ out_file = open(str(file_name), "w")
260
+ out_file.write(data_to_write)
261
+ out_file.close()
pynmrstar/_types.py ADDED
@@ -0,0 +1,14 @@
1
+ from typing import Dict, Any, List, Union
2
+
3
+ RowDict = Dict[str, Any] # One row: tag → value
4
+ ColumnarDict = Dict[str, List[Any]] # One column per tag
5
+ RowMatrix = List[List[Any]] # Matrix of rows (format #3)
6
+ FlatRow = List[Any] # One flat row (format #4)
7
+
8
+ DataInput = Union[
9
+ RowDict, # format #1 – single row
10
+ List[RowDict], # format #1 – many rows
11
+ ColumnarDict, # format #2 – columnar
12
+ RowMatrix, # format #3 – list of lists
13
+ FlatRow, # format #4 – flat list
14
+ ]
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/python3
2
+
3
+ """ NMR-STAR definitions and other module parameters live here. Technically
4
+ you can edit them, but you should really know what you're doing.
5
+
6
+ Adding key->value pairs to STR_CONVERSION_DICT will automatically convert tags
7
+ whose value matches "key" to the string "value" when printing. This allows you
8
+ to set the default conversion value for Booleans or other objects.
9
+
10
+ WARNINGS:
11
+ * STR_CONVERSION_DICT cannot contain both booleans and arithmetic types.
12
+ Attempting to use both will cause an issue since boolean True == 1 in python
13
+ and False == 0.
14
+
15
+ * You must call utils.quote_value.clear_cache() after changing the
16
+ STR_CONVERSION_DICT or else your changes won't take effect due to caching!
17
+
18
+ The only exception is if you set STR_CONVERSION_DICT before performing any
19
+ actions which would call quote_value() - which include calling __str__ or
20
+ format() on Entry, Saveframe, and Loop objects.
21
+ """
22
+
23
+ NULL_VALUES = ['', ".", "?", None]
24
+ WHITESPACE: str = " \t\n\v"
25
+ RESERVED_KEYWORDS = ["data_", "save_", "loop_", "stop_", "global_"]
26
+ STR_CONVERSION_DICT: dict = {None: "."}
27
+
28
+ API_URL: str = "https://api.bmrb.io/v2"
29
+ SCHEMA_URL: str = 'https://raw.githubusercontent.com/uwbmrb/nmr-star-dictionary/master/xlschem_ann.csv'
30
+ COMMENT_URL: str = "https://raw.githubusercontent.com/uwbmrb/PyNMRSTAR/v3/reference_files/comments.str"
31
+ TYPES_URL: str = "https://raw.githubusercontent.com/uwbmrb/PyNMRSTAR/v3/pynmrstar/reference_files/data_types.csv"
32
+ FTP_URL: str = "https://bmrb.io/ftp/pub/bmrb"