Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add location oid type #145

Merged
merged 19 commits into from
Jul 18, 2024
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
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ on:
jobs:
tests:
name: "Python ${{ matrix.python-version }}"
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
env:
USING_COVERAGE: '3.7,3.8,3.10'
USING_COVERAGE: '3.7,3.8,3.10,3.11,3.12'

strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:
# Install dependencies. `--no-root` means "install all dependencies but not the project
# itself", which is what you want to avoid caching _your_ code. The `if` statement
# ensures this only runs on a cache miss.
- run: poetry install --no-interaction --no-root
- run: poetry install --no-interaction --no-root --with dev
if: steps.cache-deps.outputs.cache-hit != 'true'

# Now install _your_ project. This isn't necessary for many types of projects -- particularly
Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ Developer Setup
```
git clone https://github.com/mdsol/rwslib
```
2. Create a Virtual Env for the Local instance
2. Install poetry and start a shell
```bash
$ python -m venv venv
$ source venv/bin/activate
$ pip install poetry
$ poetry shell
```
3. Install the development dependencies
```bash
$ pip install -r requirements-dev.txt
$ poetry install --with dev
```
4. Enjoy !!!

Expand Down
526 changes: 280 additions & 246 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rwslib"
version = "1.2.11"
version = "1.2.12"
description = "Rave Web Services for Python"
authors = ["Ian Sparks <[email protected]>"]
maintainers = ["Geoff Low <[email protected]>"]
Expand All @@ -14,11 +14,9 @@ classifiers = [
'Natural Language :: English',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
]
packages = [
{ include = "rwslib" },
Expand All @@ -33,19 +31,21 @@ click = "*"
faker = "*"
urllib3 = "*"

[tool.poetry.group.dev]
optional = true

[tool.poetry.group.dev.dependencies]
httpretty = "*"
mock = "*"
coverage = "*"
pytest = "*"
tox = "*"
tox-poetry = "*"

[tool.poetry.group.docs.dependencies]
Sphinx = "^5.1.1"
sphinx-pyproject = "^0.3.0"

[build-system]
requires = ["poetry-core"]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

62 changes: 58 additions & 4 deletions rwslib/builders/clinicaldata.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from typing import Optional

from rwslib.builders.common import (
ODMElement,
TransactionalElement,
Expand All @@ -8,7 +10,7 @@
)
from rwslib.builders.modm import LastUpdateMixin, MilestoneMixin
from rwslib.builders.metadata import MeasurementUnitRef
from rwslib.builders.constants import ProtocolDeviationStatus, QueryStatusType
from rwslib.builders.constants import ProtocolDeviationStatus, QueryStatusType, LocationOIDType
from rwslib.builders.common import Unset

from collections import OrderedDict
Expand Down Expand Up @@ -77,6 +79,7 @@ def __init__(
subject_key,
subject_key_type="SubjectName",
transaction_type="Update",
location_oid_type: Optional[LocationOIDType] = None,
):
"""
:param str site_location_oid: :class:`SiteLocation` OID
Expand All @@ -98,6 +101,8 @@ def __init__(
self.signature = None
#: :class:`SiteRef`
self.siteref = None
#: :class:`LocationOIDType`
self.location_oid_type = location_oid_type

def build(self, builder):
"""Build XML by appending to builder"""
Expand All @@ -120,8 +125,8 @@ def build(self, builder):
if self.siteref:
self.siteref.build(builder)
else:
builder.start("SiteRef", {"LocationOID": self.sitelocationoid})
builder.end("SiteRef")
_siteref = SiteRef(self.sitelocationoid, self.location_oid_type)
_siteref.build(builder)

for event in self.study_events:
event.build(builder)
Expand Down Expand Up @@ -993,17 +998,66 @@ class SiteRef(ODMElement, LastUpdateMixin):
.. note:: The `mdsol:LocationOIDType` attribute should be used to indicate the type of `LocationOID`
"""

def __init__(self, oid):
def __init__(self,
oid,
location_oid_type: Optional[LocationOIDType] = None,
site_number: Optional[str] = None,
study_site_name: Optional[str] = None,
site_uuid: Optional[str] = None,
location_name: Optional[str] = None,
study_env_site_number: Optional[str] = None,
previous_location_oid: Optional[str] = None,
shared_location_oid: Optional[str] = None,
previous_shared_location_oid: Optional[str] = None,
subject_created_at_site: Optional[str] = None,):
"""
:param str oid: OID for referenced :class:`Location`
:param LocationOIDType location_oid_type: Type for the oid
:param site_number: Site number
:param study_site_name: Study site name
:param site_uuid: Site UUID
:param location_name: Location name
:param study_env_site_number: Study environment site number
:param previous_location_oid: Previous location OID
:param shared_location_oid: Shared location OID
:param previous_shared_location_oid: Previous shared location OID
:param subject_created_at_site: Subject created at site
"""
self.oid = oid
self.location_oid_type = location_oid_type
self.site_number = site_number
self.study_site_name = study_site_name
self.site_uuid = site_uuid
self.location_name = location_name
self.study_env_site_number = study_env_site_number
self.previous_location_oid = previous_location_oid
self.shared_location_oid = shared_location_oid
self.previous_shared_location_oid = previous_shared_location_oid
self.subject_created_at_site = subject_created_at_site

def build(self, builder):
"""
Build XML by appending to builder
"""
params = dict(LocationOID=self.oid)
if self.location_oid_type:
params["mdsol:LocationOIDType"] = self.location_oid_type.value
elif self.site_uuid:
params["mdsol:SiteUUID"] = self.site_uuid
elif self.site_number:
params["mdsol:SiteNumber"] = self.site_number
elif self.study_site_name:
params["mdsol:StudySiteName"] = self.study_site_name
elif self.study_env_site_number:
params["mdsol:StudyEnvSiteNumber"] = self.study_env_site_number
if self.location_name:
params["mdsol:LocationName"] = self.location_name
if self.previous_location_oid:
params["mdsol:PreviousLocationOID"] = self.previous_location_oid
if self.shared_location_oid:
params["mdsol:SharedLocationOID"] = self.shared_location_oid
if self.subject_created_at_site:
params["mdsol:SubjectCreatedAtSite"] = self.subject_created_at_site
# mixins
self.mixin()
self.mixin_params(params)
Expand Down
16 changes: 13 additions & 3 deletions rwslib/builders/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-

import platform
from string import ascii_letters
from datetime import datetime
import datetime
from xml.etree import cElementTree as ET


Expand All @@ -13,11 +13,21 @@

# -----------------------------------------------------------------------------------------------------------------------
# Utilities
def get_utc_date() -> datetime.datetime:
"""
Returns the UTC date as datetime.datetime object.
"""
version = platform.python_version_tuple()
if int(version[1]) < 11:
utc_date = datetime.datetime.utcnow()
else:
utc_date = datetime.datetime.now(datetime.UTC)
return utc_date


def now_to_iso8601():
"""Returns NOW date/time as a UTC date/time formated as iso8601 string"""
utc_date = datetime.utcnow()
utc_date = get_utc_date()
return dt_to_iso8601(utc_date)


Expand Down
11 changes: 11 additions & 0 deletions rwslib/builders/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,14 @@ class GranularityType(enum.Enum):
AllClinicalData = 'AllClinicalData'
SingleSite = 'SingleSite'
SingleSubject = 'SingleSubject'


class LocationOIDType(enum.Enum):
"""
Location OID Type Enumeration
Applies to a :class:`SiteRef`
"""
SiteUUID = 'SiteUUID'
SiteNumber = 'SiteNumber'
StudyEnvSiteNumber = 'StudyEnvSiteNumber'

4 changes: 3 additions & 1 deletion rwslib/builders/modm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import datetime
import enum

from rwslib.builders.common import get_utc_date


class MODMExtensionRegistry(enum.Enum):
"""
Expand Down Expand Up @@ -167,7 +169,7 @@ def set_update_time(self, update_time=None):
if update_time and isinstance(update_time, (datetime.datetime,)):
self.last_update_time = update_time
else:
self.last_update_time = datetime.datetime.utcnow()
self.last_update_time = get_utc_date()

def mixin_params(self, params):
"""
Expand Down
12 changes: 0 additions & 12 deletions rwslib/tests/__init__.py

This file was deleted.

Empty file added tests/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest
import os


@pytest.fixture(scope="class")
def car_message(request):
with open(
os.path.join(
os.path.dirname(__file__), "fixtures", "car_message.xml"
), 'r'
) as fh:
content = fh.read()
request.cls.car_message = content


@pytest.fixture(scope="class")
def double_byte_chars(request):
with open(os.path.join(
os.path.dirname(__file__), "fixtures", "test_double_byte_chars.xml"), "r+b") as fh:
content = fh.read()
request.cls.double_byte_chars = content
File renamed without changes.
Empty file added tests/rwslib/__init__.py
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

from mock import patch

from rwslib.tests.common import obj_to_doc
from ..common import obj_to_doc

__author__ = 'isparks'

import unittest

from rwslib.builders.common import bool_to_yes_no, bool_to_true_false, ODMElement
from rwslib.builders.common import bool_to_yes_no, bool_to_true_false, ODMElement, get_utc_date
from rwslib.builders.clinicaldata import UserRef, LocationRef, ClinicalData, SubjectData
from rwslib.builders.metadata import Study
from rwslib.builders.admindata import AdminData
Expand Down Expand Up @@ -123,7 +123,7 @@ def test_creation_datetime(self):
tested_1 = obj_to_doc(obj=obj_1)
with patch('rwslib.builders.common.datetime') as mock_dt:
# offset the time to ensure we don't overlap
mock_dt.utcnow.return_value = datetime.datetime.utcnow() + datetime.timedelta(seconds=61)
mock_dt.utcnow.return_value = get_utc_date() + datetime.timedelta(seconds=61)
obj_2 = ODM("Test User", fileoid="1235", source_system="Battlestar", source_system_version="1.04")
tested_2 = obj_to_doc(obj=obj_2)
self.assertEqual(tested_1.get('Originator'), tested_2.get('Originator'))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
import datetime

from rwslib.builders.common import get_utc_date
from rwslib.builders.constants import LocationType, UserType

__author__ = 'glow'

import unittest
from rwslib.tests.common import obj_to_doc
from ..common import obj_to_doc
from rwslib.builders.admindata import AdminData, User, FirstName, LastName, Location, DisplayName, MetaDataVersionRef


Expand Down Expand Up @@ -125,33 +126,40 @@ def test_create_user_with_display_nae(self):
self.assertEqual("DisplayName", list(tested)[0].tag)
self.assertEqual("Henrik", list(tested)[0].text)


class TestMetaDataVersionRef(unittest.TestCase):

def test_create_a_version_ref(self):
"""We create a MetaDataVersionRef"""
obj = MetaDataVersionRef("Mediflex(Prod)", "1024", datetime.datetime.utcnow())
_utc_date = get_utc_date()
obj = MetaDataVersionRef("Mediflex(Prod)", "1024", _utc_date)
tested = obj_to_doc(obj)
self.assertEqual("MetaDataVersionRef", tested.tag)
self.assertEqual("Mediflex(Prod)", tested.get('StudyOID'))
self.assertEqual("1024", tested.get('MetaDataVersionOID'))
self.assertTrue(tested.get('EffectiveDate').startswith(datetime.date.today().isoformat()))
_effective_date = tested.get('EffectiveDate')
self.assertIsNotNone(_effective_date)
_ref_date = get_utc_date().isoformat().split('T')[0]
self.assertTrue(_effective_date.startswith(_ref_date))

def test_create_a_version_ref_and_attach_to_location(self):
"""We create a MetaDataVersionRef"""
this = MetaDataVersionRef("Mediflex(Prod)", "1024", datetime.datetime.utcnow() - datetime.timedelta(days=7))
that = MetaDataVersionRef("Mediflex(Prod)", "1025", datetime.datetime.utcnow())
this = MetaDataVersionRef("Mediflex(Prod)",
"1024", get_utc_date() - datetime.timedelta(days=7))
that = MetaDataVersionRef("Mediflex(Prod)", "1025", get_utc_date())
obj = Location('Site01', 'Site 1')
obj << this
obj << that
obj << that
tested = obj_to_doc(obj)
self.assertEqual("Location", tested.tag)
self.assertTrue(len(list(tested)) == 2)
_this = list(tested)[0]
self.assertEqual("Mediflex(Prod)", _this.get('StudyOID'))
self.assertEqual("1024", _this.get('MetaDataVersionOID'))
self.assertTrue(_this.get('EffectiveDate').startswith((datetime.date.today() -
datetime.timedelta(days=7)).isoformat()))
_ref_date = (get_utc_date() - datetime.timedelta(days=7))
self.assertTrue(_this.get('EffectiveDate').startswith(_ref_date.isoformat().split('T')[0]))
_that = list(tested)[1]
self.assertEqual("Mediflex(Prod)", _that.get('StudyOID'))
self.assertEqual("1025", _that.get('MetaDataVersionOID'))
self.assertTrue(_that.get('EffectiveDate').startswith(datetime.date.today().isoformat()))
_ref_date = get_utc_date()
self.assertTrue(_that.get('EffectiveDate').startswith(_ref_date.isoformat().split('T')[0]))
Loading
Loading