sparclclient 1.2.2b10__tar.gz → 1.2.3__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 (38) hide show
  1. sparclclient-1.2.3/.github/workflows/django.yml +33 -0
  2. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/PKG-INFO +5 -1
  3. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/pyproject.toml +1 -0
  4. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/requirements-client.txt +2 -1
  5. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/source/conf.py +2 -3
  6. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/__init__.py +2 -1
  7. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/client.py +55 -7
  8. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/tests/expected_pat.py +31 -15
  9. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/tests/tests_api.py +47 -10
  10. sparclclient-1.2.2b10/.github/workflows/django.yml +0 -30
  11. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/.gitignore +0 -0
  12. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/.pre-commit-config.yaml +0 -0
  13. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/.readthedocs.yaml +0 -0
  14. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/LICENSE +0 -0
  15. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/Makefile +0 -0
  16. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/README.md +0 -0
  17. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/make.bat +0 -0
  18. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/requirements-internal.txt +0 -0
  19. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/requirements.txt +0 -0
  20. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/source/index.rst +0 -0
  21. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/source/sparcl.rst +0 -0
  22. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/Results.py +0 -0
  23. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/benchmarks/__init__.py +0 -0
  24. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/benchmarks/benchmarks.py +0 -0
  25. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/conf.py +0 -0
  26. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/exceptions.py +0 -0
  27. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/fields.py +0 -0
  28. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/gather_2d.py +0 -0
  29. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/notebooks/sparcl-examples.ipynb +0 -0
  30. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/resample_spectra.py +0 -0
  31. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/sparc.ini +0 -0
  32. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/type_conversion.py +0 -0
  33. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/unsupported.py +0 -0
  34. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/sparcl/utils.py +0 -0
  35. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/tests/expected_dev1.py +0 -0
  36. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/tests/methods_tests.py +0 -0
  37. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/tests/utils.py +0 -0
  38. {sparclclient-1.2.2b10 → sparclclient-1.2.3}/tox.ini +0 -0
@@ -0,0 +1,33 @@
1
+ # This workflow will install Python dependencies and run unittests with 2 Python versions
2
+
3
+ name: ci-django-unittests
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ branches: [main]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ max-parallel: 4
16
+ matrix:
17
+ python-version: [3.9, '3.10']
18
+
19
+ steps:
20
+ - uses: actions/checkout@v3
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v3
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ - name: Install pip dependencies
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ - name: Install requirements dependencies
29
+ run: |
30
+ python -m pip install -r requirements-client.txt
31
+ - name: Run unittests
32
+ run: |
33
+ python -m unittest tests.tests_api
@@ -1,10 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sparclclient
3
- Version: 1.2.2b10
3
+ Version: 1.2.3
4
4
  Summary: A client for getting spectra and meta-data from NOIRLab.
5
5
  Author-email: "S. Pothier" <datalab-spectro@noirlab.edu>
6
6
  Description-Content-Type: text/markdown
7
7
  Classifier: License :: OSI Approved :: MIT License
8
+ Requires-Dist: requests==2.31.0
9
+ Requires-Dist: numpy>=1.23.5,<1.26.4
10
+ Requires-Dist: spectres
11
+ Requires-Dist: pyjwt
8
12
  Project-URL: Bug Tracker, https://github.com/pypa/sparclclient/issues
9
13
  Project-URL: Documentation, https://sparclclient.readthedocs.io/en/latest/
10
14
  Project-URL: Homepage, https://github.com/pypa/sparclclient
@@ -24,6 +24,7 @@ license = {file = "LICENSE"}
24
24
  classifiers = ["License :: OSI Approved :: MIT License"]
25
25
  # version is found in: ~/sandbox/sparclclient/sparcl/__init__.py
26
26
  dynamic = ["version", "description"]
27
+ dependencies = ["requests==2.31.0", "numpy>=1.23.5,<1.26.4", "spectres", "pyjwt"]
27
28
 
28
29
  [project.urls]
29
30
  "Homepage" = "https://github.com/pypa/sparclclient"
@@ -1,3 +1,4 @@
1
1
  requests==2.31.0 # 2.26.0
2
- numpy
2
+ numpy>=1.23.5,<1.26.4
3
3
  spectres
4
+ pyjwt
@@ -14,14 +14,13 @@
14
14
  import os
15
15
  import re
16
16
  import sys
17
- from sparcl import __version__
18
-
19
17
  sys.path.insert(0, os.path.abspath(".."))
18
+ from sparcl import __version__
20
19
 
21
20
  # -- Project information -----------------------------------------------------
22
21
 
23
22
  project = "SPARCL"
24
- copyright = "2022, S.Pothier, A.Jacques"
23
+ copyright = "2024, S.Pothier, A.Jacques"
25
24
  author = "S.Pothier, A.Jacques"
26
25
 
27
26
  #!version = client_version
@@ -31,4 +31,5 @@ __all__ = ["client", "align_records"]
31
31
  # __version__ = "1.2.1b3"
32
32
  # __version__ = "1.2.1"
33
33
  # FIRST uncommented value will be used! (so only leave one uncommented)
34
- __version__ = "1.2.2b10"
34
+ #__version__ = "1.2.2"
35
+ __version__ = "1.2.3"
@@ -24,6 +24,7 @@ from urllib.parse import urlencode, urlparse
24
24
  from warnings import warn
25
25
  import pickle
26
26
  import getpass
27
+ import datetime
27
28
 
28
29
  #!from pathlib import Path
29
30
  import tempfile
@@ -31,6 +32,7 @@ import tempfile
31
32
  ############################################
32
33
  # External Packages
33
34
  import requests
35
+ import jwt
34
36
 
35
37
  #!from requests.auth import HTTPBasicAuth
36
38
  from requests.auth import AuthBase
@@ -118,12 +120,15 @@ RESERVED = set([DEFAULT, ALL])
118
120
  class TokenAuth(AuthBase):
119
121
  """Attaches HTTP Token Authentication to the given Request object."""
120
122
 
121
- def __init__(self, token):
123
+ def __init__(self, token, renew_check):
122
124
  # setup any auth-related data here
123
125
  self.token = token
126
+ self.renew_check = renew_check # SparcClient Method
124
127
 
125
128
  def __call__(self, request):
126
129
  # modify and return the request
130
+ # check and renew token if needed
131
+ self.renew_check(renew=True)
127
132
  request.headers["Authorization"] = self.token
128
133
  return request
129
134
 
@@ -163,7 +168,7 @@ class SparclClient: # was SparclApi()
163
168
 
164
169
  """
165
170
 
166
- KNOWN_GOOD_API_VERSION = 11.0 # @@@ Change when Server version incremented
171
+ KNOWN_GOOD_API_VERSION = 12.0 # @@@ Change when Server version incremented
167
172
 
168
173
  def __init__(
169
174
  self,
@@ -182,6 +187,8 @@ class SparclClient: # was SparclApi()
182
187
  self.apiurl = f"{self.rooturl}/sparc"
183
188
  self.apiversion = None
184
189
  self.token = None
190
+ self.refresh_token = None
191
+ self.token_exp = None
185
192
  self.verbose = verbose
186
193
  self.show_curl = show_curl # Show CURL equivalent of client method
187
194
  #!self.internal_names = internal_names
@@ -243,6 +250,35 @@ class SparclClient: # was SparclApi()
243
250
  f" read_timeout={self.r_timeout})"
244
251
  )
245
252
 
253
+ def token_expired(self, renew=False):
254
+ """
255
+ POST http://localhost:8050/sparc/renew_token/
256
+ Content-Type: application/json
257
+ {
258
+ "refresh_token": "..."
259
+ }
260
+
261
+ Returns an 'access' token
262
+ """
263
+ now = datetime.datetime.now()
264
+ expired = True
265
+ if self.token_exp is None or self.token_exp <= now:
266
+ expired = True
267
+ else:
268
+ expired = False
269
+
270
+ if expired and renew:
271
+ url = f"{self.apiurl}/renew_token/"
272
+ resp = requests.post(url, json={"refresh_token": self.renew_token})
273
+ resp.raise_for_status()
274
+ data = resp.json()
275
+ #@print(f"{data=}")
276
+ self.token = data['access']
277
+ self.set_token_exp()
278
+ expired = False
279
+
280
+ return expired
281
+
246
282
  def login(self, email, password=None):
247
283
  """Login to the SPARCL service.
248
284
 
@@ -284,20 +320,32 @@ class SparclClient: # was SparclApi()
284
320
  try:
285
321
  res.raise_for_status()
286
322
  #!print(f"DBG: {res.content=}")
287
- self.token = res.json()
323
+ self.token = res.json()['access']
324
+ self.renew_token = res.json()['refresh']
288
325
  self.session.auth = (email, password)
289
326
  except Exception:
290
327
  self.session.auth = None
291
328
  self.token = None
329
+ self.renew_token = None
330
+ self.token_exp = None
292
331
  msg = (
293
332
  "Could not login with given credentials."
294
333
  ' Reverted to "Anonymous" user.'
295
334
  )
296
335
  return msg
297
336
 
337
+ self.set_token_exp()
298
338
  print(f"Logged in successfully with {email=}")
299
339
  return None
300
340
 
341
+ def set_token_exp(self):
342
+ decoded = jwt.decode(
343
+ self.token,
344
+ algorithms=['HS256',],
345
+ options={'verify_signature': False}
346
+ )
347
+ self.token_exp = datetime.datetime.fromtimestamp(decoded['exp'])
348
+
301
349
  def logout(self):
302
350
  """Logout of the SPARCL service.
303
351
 
@@ -317,7 +365,7 @@ class SparclClient: # was SparclApi()
317
365
 
318
366
  @property
319
367
  def authorized(self):
320
- auth = TokenAuth(self.token) if self.token else None
368
+ auth = TokenAuth(self.token, self.token_expired) if self.token else None # noqa: E501
321
369
  response = requests.get(
322
370
  f"{self.apiurl}/auth_status/", auth=auth, timeout=self.timeout
323
371
  )
@@ -468,7 +516,7 @@ class SparclClient: # was SparclApi()
468
516
  Example:
469
517
  >>> client = SparclClient()
470
518
  >>> client.version
471
- 11.0
519
+ 12.0
472
520
  """
473
521
 
474
522
  if self.apiversion is None:
@@ -567,7 +615,7 @@ class SparclClient: # was SparclApi()
567
615
  cmd = ut.curl_find_str(sspec, self.rooturl, qstr=qstr)
568
616
  print(cmd)
569
617
 
570
- auth = TokenAuth(self.token) if self.token else None
618
+ auth = TokenAuth(self.token, self.token_expired) if self.token else None # noqa: E501
571
619
  res = requests.post(url, json=sspec, auth=auth, timeout=self.timeout)
572
620
 
573
621
  if res.status_code != 200:
@@ -833,7 +881,7 @@ class SparclClient: # was SparclApi()
833
881
  print(cmd)
834
882
 
835
883
  try:
836
- auth = TokenAuth(self.token) if self.token else None
884
+ auth = TokenAuth(self.token, self.token_expired) if self.token else None # noqa: E501
837
885
  res = requests.post(url, json=ids, auth=auth, timeout=self.timeout)
838
886
  except requests.exceptions.ConnectTimeout as reCT:
839
887
  raise ex.UnknownSparcl(f"ConnectTimeout: {reCT}")
@@ -40,11 +40,20 @@ retrieve_0b = ["_dr", "dec", "flux", "ra", "sparcl_id", "specid", "wavelength"]
40
40
 
41
41
  retrieve_5 = 2
42
42
 
43
+ # OLD as of August 23, 2024
44
+ #find_0 = [
45
+ # {
46
+ # "_dr": "BOSS-DR16",
47
+ # "data_release": "BOSS-DR16",
48
+ # "specid": -6444532452352045056,
49
+ # }
50
+ #]
51
+
43
52
  find_0 = [
44
53
  {
45
- "_dr": "BOSS-DR16",
46
- "data_release": "BOSS-DR16",
47
- "specid": -6444532452352045056,
54
+ '_dr': 'BOSS-DR16',
55
+ 'data_release': 'BOSS-DR16',
56
+ 'telescope': 'sloan25m'
48
57
  }
49
58
  ]
50
59
 
@@ -58,11 +67,12 @@ find_1 = [
58
67
 
59
68
  find_2 = 936894 # PAT
60
69
 
61
- find_3 = [
62
- {"_dr": "BOSS-DR16", "data_release": "BOSS-DR16"},
63
- {"_dr": "DESI-EDR", "data_release": "DESI-EDR"},
64
- {"_dr": "SDSS-DR16", "data_release": "SDSS-DR16"},
65
- ]
70
+ # OLD as of August 23, 2024
71
+ #find_3 = [
72
+ # {"_dr": "BOSS-DR16", "data_release": "BOSS-DR16"},
73
+ # {"_dr": "DESI-EDR", "data_release": "DESI-EDR"},
74
+ # {"_dr": "SDSS-DR16", "data_release": "SDSS-DR16"},
75
+ #]
66
76
 
67
77
  find_4 = 36
68
78
 
@@ -77,6 +87,7 @@ authorized_1 = {
77
87
  "Loggedin_As": "test_user_1@noirlab.edu",
78
88
  "Authorized_Datasets": {
79
89
  "BOSS-DR16",
90
+ "DESI-DR1",
80
91
  "DESI-EDR",
81
92
  "SDSS-DR16",
82
93
  "SDSS-DR17-test",
@@ -96,15 +107,20 @@ authorized_3 = {
96
107
  # Private and Public
97
108
  pub_1 = ["BOSS-DR16"]
98
109
  pub_all = ["BOSS-DR16", "DESI-EDR", "SDSS-DR16"]
99
- priv = ["SDSS-DR17-test"]
110
+ priv = ["DESI-DR1", "SDSS-DR17-test"]
111
+ all_all = pub_all + priv
112
+ all_all.sort()
100
113
  unauth = "test_user_2@noirlab.edu"
101
114
  #
102
- auth_find_1 = auth_find_2 = pub_all + priv
103
- auth_find_3 = f"[DSDENIED] {unauth} is declined access to datasets {priv}"
115
+ auth_find_1 = auth_find_2 = all_all
116
+ auth_find_3 = auth_retrieve_3 = (f"[DSDENIED] uname='{unauth}' is declined "
117
+ f"access to datasets={priv}; "
118
+ f"drs_requested={all_all} "
119
+ f"my_auth={pub_all}")
104
120
  auth_find_4 = auth_find_6 = pub_all
105
- auth_find_5 = f"[DSDENIED] ANONYMOUS is declined access to datasets {priv}"
106
- #
121
+ auth_find_5 = auth_retrieve_6 = ("[DSDENIED] uname='ANONYMOUS' is declined "
122
+ f"access to datasets={priv}; "
123
+ f"drs_requested={all_all} "
124
+ f"my_auth={pub_all}")
107
125
  auth_retrieve_1 = auth_retrieve_2 = pub_1 + priv
108
- auth_retrieve_3 = f"[DSDENIED] {unauth} is declined access to datasets {priv}"
109
126
  auth_retrieve_4 = auth_retrieve_5 = auth_retrieve_7 = auth_retrieve_8 = pub_1
110
- auth_retrieve_6 = f"[DSDENIED] ANONYMOUS is declined access to datasets {priv}"
@@ -372,14 +372,14 @@ class SparclClientTest(unittest.TestCase):
372
372
  def test_find_0(self):
373
373
  """Get metadata using search spec."""
374
374
 
375
- outfields = ["data_release", "specid"]
375
+ outfields = ["data_release", "telescope"]
376
376
  # To get suitable constraints (in sparc-shell on Server):
377
377
  # list(FitsRecord.objects.all().values('ra','dec'))
378
- constraints = {"ra": [132.1, 132.2], "dec": [+28.0, +28.1]}
378
+ constraints = {"data_release": ['BOSS-DR16']}
379
379
  found = self.client.find(outfields, constraints=constraints, limit=3)
380
- actual = found.records[:2]
380
+ actual = found.records[:1]
381
381
  if showact:
382
- print(f"find_0: actual={pf(actual[:2])}")
382
+ print(f"find_0: actual={pf(actual[:1])}")
383
383
  self.assertEqual(actual, exp.find_0, msg="Actual to Expected")
384
384
 
385
385
  @skip("fiddly bit skipped until we use factoryboy")
@@ -421,10 +421,10 @@ class SparclClientTest(unittest.TestCase):
421
421
  found = self.client.find(outfields, limit=3, sort="data_release")
422
422
  actual = sorted(found.records, key=lambda rec: rec["data_release"])
423
423
  if showact:
424
- print(f"find_3: actual={pf(actual)}")
424
+ print(f"find_3: actual={pf(actual)}, len(actual)={len(actual)}")
425
425
  self.assertEqual(
426
- actual,
427
- sorted(exp.find_3, key=lambda rec: rec["data_release"]),
426
+ len(actual),
427
+ 3,
428
428
  msg="Actual to Expected",
429
429
  )
430
430
 
@@ -757,8 +757,9 @@ class AuthTest(unittest.TestCase):
757
757
 
758
758
  # Dataset lists
759
759
  cls.Pub = ["BOSS-DR16", "DESI-EDR", "SDSS-DR16"]
760
- cls.Priv = ["SDSS-DR17-test"]
760
+ cls.Priv = ["DESI-DR1", "SDSS-DR17-test"]
761
761
  cls.PrivPub = cls.Priv + cls.Pub
762
+ cls.PrivPub.sort()
762
763
 
763
764
  # Sample list of sparcl_ids from each data set
764
765
  out = ["sparcl_id"]
@@ -843,15 +844,51 @@ class AuthTest(unittest.TestCase):
843
844
  if showact:
844
845
  print(f"test_get_token: {json=}")
845
846
 
846
- expected = 281
847
+ expected = 279
847
848
  res = requests.post(f"{self.client.apiurl}/get_token/", json=json)
848
849
  self.assertEqual(res.status_code, 200, res.content.decode())
849
- token = res.content.decode()
850
+ token = res.json()['access']
850
851
  actual = len(token)
851
852
  if showact:
852
853
  print(f"test_get_token: ({len(token)}) {token=!s}")
853
854
  self.assertEqual(actual, expected, msg="Actual to Expected")
854
855
 
856
+ def getToken(self):
857
+ json = {"email": self.auth_user, "password": usrpw}
858
+ res = requests.post(f"{self.client.apiurl}/get_token/", json=json)
859
+ token = res.json()
860
+ self.client.token = token['access']
861
+ self.client.renew_token = token['refresh']
862
+
863
+ def test_token_not_expired(self):
864
+ self.getToken()
865
+ expired = self.client.token_expired(renew=False)
866
+ self.assertEqual(expired, False, "Token not expired")
867
+
868
+ def test_token_expired(self):
869
+ self.getToken()
870
+ if showact:
871
+ print(f"Token: {self.client.token=!s}")
872
+
873
+ self.client.token_exp = datetime.datetime.now()
874
+ expired = self.client.token_expired(renew=False)
875
+ self.assertEqual(expired, True, "Token expired")
876
+
877
+ def test_token_expired_renew(self):
878
+ """
879
+ POST http://localhost:8050/sparc/renew_token/
880
+ Content-Type: application/json
881
+ {
882
+ "token": "..."
883
+ }
884
+
885
+ """
886
+ self.getToken()
887
+ self.client.token_exp = datetime.datetime.now()
888
+
889
+ expired = self.client.token_expired(renew=True)
890
+ self.assertEqual(expired, False, "Token expired, renewed")
891
+
855
892
  def test_authorized_1(self):
856
893
  """Test authorized method with authorized user signed in"""
857
894
  self.silent_login(self.auth_user, usrpw)
@@ -1,30 +0,0 @@
1
- name: Django CI
2
-
3
- on:
4
- push:
5
- branches: [ "main" ]
6
- pull_request:
7
- branches: [ "main" ]
8
-
9
- jobs:
10
- build:
11
-
12
- runs-on: ubuntu-latest
13
- strategy:
14
- max-parallel: 4
15
- matrix:
16
- python-version: [3.7, 3.8, 3.9]
17
-
18
- steps:
19
- - uses: actions/checkout@v3
20
- - name: Set up Python ${{ matrix.python-version }}
21
- uses: actions/setup-python@v3
22
- with:
23
- python-version: ${{ matrix.python-version }}
24
- - name: Install Dependencies
25
- run: |
26
- python -m pip install --upgrade pip
27
- pip install -r requirements.txt
28
- - name: Run Tests
29
- run: |
30
- python manage.py test
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes