Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
arrow==1.3.0
boto3==1.34.35
deprecation==2.1.0
gemd==2.1.8
gemd==2.1.9
pyjwt==2.8.0
requests==2.32.0
requests==2.32.2
tqdm==4.66.3

# boto3 (through botocore) depends on urllib3. Version 1.34.35 requires
Expand Down
6 changes: 2 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,21 @@
package_dir={'': 'src'},
packages=find_packages(where='src'),
install_requires=[
"requests>=2.31.0,<3",
"requests>=2.32.2,<3",
"pyjwt>=2,<3",
"arrow>=1.0.0,<2",
"gemd>=2.1.8,<3",
"gemd>=2.1.9,<3",
"boto3>=1.34.35,<2",
"deprecation>=2.1.0,<3",
"urllib3>=1.26.18,<3",
"tqdm>=4.27.0,<5",
"pint>=0.21,<0.24"
],
extras_require={
"tests": [
"factory-boy>=3.3.0,<4",
"mock>=5.1.0,<6",
"pandas>=2.0.3,<3",
"pytest>=8.0.0,<9",
"pytz>=2024.1",
"requests-mock>=1.11.0,<2",
]
},
Expand Down
2 changes: 1 addition & 1 deletion src/citrine/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.5.0"
__version__ = "3.5.1"
20 changes: 12 additions & 8 deletions src/citrine/_session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import platform
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from json.decoder import JSONDecodeError
from logging import getLogger
from os import environ
Expand All @@ -25,7 +25,7 @@

# Choose a 5-second buffer so that there's no chance of the access token
# expiring during the check for expiration
EXPIRATION_BUFFER_MILLIS: timedelta = timedelta(milliseconds=5000)
EXPIRATION_BUFFER: timedelta = timedelta(seconds=5)
logger = getLogger(__name__)


Expand Down Expand Up @@ -53,7 +53,7 @@ def __init__(self,
self.authority = ':'.join(([host] if host else []) + ([port] if port else []))
self.refresh_token: str = refresh_token
self.access_token: Optional[str] = None
self.access_token_expiration: datetime = datetime.utcnow()
self.access_token_expiration: datetime = datetime.now(timezone.utc)

agent = "{}/{} python-requests/{} citrine-python/{}".format(
platform.python_implementation(),
Expand Down Expand Up @@ -106,7 +106,8 @@ def _versioned_base_url(self, version: str = 'v1'):
))

def _is_access_token_expired(self):
return self.access_token_expiration - EXPIRATION_BUFFER_MILLIS <= datetime.utcnow()
buffered_expire = self.access_token_expiration - EXPIRATION_BUFFER
return datetime.now(timezone.utc) > buffered_expire

def _refresh_access_token(self) -> None:
"""Optionally refresh our access token (if the previous one is about to expire)."""
Expand All @@ -118,10 +119,13 @@ def _refresh_access_token(self) -> None:
if response.status_code != 200:
raise UnauthorizedRefreshToken()
self.access_token = response.json()['access_token']
self.access_token_expiration = datetime.utcfromtimestamp(
jwt.decode(self.access_token,
options={"verify_signature": False},
algorithms=["HS256"])['exp']
self.access_token_expiration = datetime.fromtimestamp(
jwt.decode(
self.access_token,
options={"verify_signature": False},
algorithms=["HS256"]
)['exp'],
timezone.utc
)

# Explicitly set an updated 'auth', so as to not rely on implicit cookie handling.
Expand Down
4 changes: 2 additions & 2 deletions src/citrine/resources/data_concepts.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ def _path_template(self):
@property
def _dataset_agnostic_path_template(self):
if self.project_id is None:
return f'teams/{self.team_id}/{self._collection_key.replace("_","-")}'
return f'teams/{self.team_id}/{self._collection_key.replace("_", "-")}'
else:
return f'projects/{self.project_id}/{self._collection_key.replace("_","-")}'
return f'projects/{self.project_id}/{self._collection_key.replace("_", "-")}'

def build(self, data: dict) -> ResourceType:
"""
Expand Down
1 change: 0 additions & 1 deletion test_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ flake8-docstrings==1.7.0
mock==5.1.0
pytest==8.0.0
pytest-cov==4.1.0
pytz==2024.1
requests-mock==1.11.0

# faker is a dependency of factory-boy, but factory-boy sets a very low floor
Expand Down
5 changes: 2 additions & 3 deletions tests/test_citrine.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import platform
from datetime import datetime
from datetime import datetime, timezone

import jwt
import pytest
import pytz
import requests_mock

from citrine import Citrine
Expand All @@ -17,7 +16,7 @@ def refresh_token(expiration: datetime = None) -> dict:
return {'access_token': token}


token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=pytz.utc))
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc))


def test_citrine_creation():
Expand Down
26 changes: 11 additions & 15 deletions tests/test_session.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import json

import jwt
import pytest
import unittest

from citrine.exceptions import (
BadRequest,
CitrineException,
Conflict,
NonRetryableException,
WorkflowNotReadyException,
RetryableException)

from datetime import datetime, timedelta
import pytz
from datetime import datetime, timedelta, timezone

import mock
import requests
import requests_mock
Expand All @@ -32,7 +28,7 @@ def refresh_token(expiration: datetime = None) -> dict:

@pytest.fixture
def session():
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=pytz.utc))
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc))
with requests_mock.Mocker() as m:
m.post('http://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response)
session = Session(
Expand All @@ -43,13 +39,13 @@ def session():
# Default behavior is to *not* require a refresh - those tests can clear this out
# As rule of thumb, we should be using freezegun or similar to never rely on the system clock
# for these scenarios, but I thought this is light enough to postpone that for the time being
session.access_token_expiration = datetime.utcnow() + timedelta(minutes=3)
session.access_token_expiration = datetime.now(timezone.utc) + timedelta(minutes=3)

return session


def test_session_signature(monkeypatch):
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=pytz.utc))
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc))
with requests_mock.Mocker() as m:
m.post('ftp://citrine-testing.fake:8080/api/v1/tokens/refresh', json=token_refresh_response)

Expand Down Expand Up @@ -77,8 +73,8 @@ def test_session_signature(monkeypatch):


def test_get_refreshes_token(session: Session):
session.access_token_expiration = datetime.utcnow() - timedelta(minutes=1)
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=pytz.utc))
session.access_token_expiration = datetime.now(timezone.utc) - timedelta(minutes=1)
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc))

with requests_mock.Mocker() as m:
m.post('http://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response)
Expand All @@ -89,11 +85,11 @@ def test_get_refreshes_token(session: Session):
resp = session.get_resource('/foo')

assert {'foo': 'bar'} == resp
assert datetime(2019, 3, 14) == session.access_token_expiration
assert datetime(2019, 3, 14, tzinfo=timezone.utc) == session.access_token_expiration


def test_get_refresh_token_failure(session: Session):
session.access_token_expiration = datetime.utcnow() - timedelta(minutes=1)
session.access_token_expiration = datetime.now(timezone.utc) - timedelta(minutes=1)

with requests_mock.Mocker() as m:
m.post('http://citrine-testing.fake/api/v1/tokens/refresh', status_code=401)
Expand Down Expand Up @@ -197,7 +193,7 @@ def test_connection_error(session: Session):


def test_post_refreshes_token_when_denied(session: Session):
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=pytz.utc))
token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc))

with requests_mock.Mocker() as m:
m.post('http://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response)
Expand All @@ -209,7 +205,7 @@ def test_post_refreshes_token_when_denied(session: Session):
resp = session.post_resource('/foo', json={'data': 'hi'})

assert {'foo': 'bar'} == resp
assert datetime(2019, 3, 14) == session.access_token_expiration
assert datetime(2019, 3, 14, tzinfo=timezone.utc) == session.access_token_expiration


# this test exists to provide 100% coverage for the legacy 401 status on Unauthorized responses
Expand Down