Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
15188af
refactor: update contact schemas
jacob-a-brown Aug 14, 2025
e6392df
refactor: created_at is inherited from ORMBaseModel
jacob-a-brown Aug 14, 2025
0e4c4e3
fix: delete emails/phones/addresses/thing association upon contact de…
jacob-a-brown Aug 14, 2025
e87d528
fix: delete contact association upon thing deletion
jacob-a-brown Aug 14, 2025
392d183
fix: set back_populates for correct relationships
jacob-a-brown Aug 14, 2025
5910111
refactor: update test_add_contact
jacob-a-brown Aug 14, 2025
de44035
refactor: differentiate add contact from fixture
jacob-a-brown Aug 14, 2025
44a4989
feat: add contact fixture
jacob-a-brown Aug 14, 2025
005b698
feat: add address, phone, email fixtures
jacob-a-brown Aug 14, 2025
7503eab
feat: add_address function
jacob-a-brown Aug 14, 2025
26582a2
feat: add POST /{contact_id}/address endpoint
jacob-a-brown Aug 14, 2025
38e1013
feat: test adding contact addresses
jacob-a-brown Aug 14, 2025
4b6a2b3
feat: implement POST endpoint and test for contact email
jacob-a-brown Aug 14, 2025
208225c
feat: implement POST endpoint and test for contact phone
jacob-a-brown Aug 14, 2025
e49ab42
feat: implement and test validators for email/phone
jacob-a-brown Aug 14, 2025
e638a04
fix: validated fields must be defined in validate class
jacob-a-brown Aug 14, 2025
390da0a
fix: validated fields must be defined in validate class
jacob-a-brown Aug 14, 2025
6074697
fix: validated fields must be defined in validate class
jacob-a-brown Aug 14, 2025
c3ccdeb
refactor: update test_get_contact_by_id to use fixtures
jacob-a-brown Aug 14, 2025
ce0f3da
feat: test 404 get contact by id
jacob-a-brown Aug 14, 2025
f998072
refactor: remove old error message
jacob-a-brown Aug 14, 2025
d1e585e
test: implement test_get_contact_emails
jacob-a-brown Aug 14, 2025
9371572
test: test get contact items 404 contact not found
jacob-a-brown Aug 14, 2025
c0eb6e1
feat: include contact_id in contact item responses
jacob-a-brown Aug 14, 2025
3917689
feat: enable get contact items as lists and by id
jacob-a-brown Aug 14, 2025
8feebd9
feat: enable contact items to change contact id
jacob-a-brown Aug 14, 2025
5482ea1
test: implement patch contact and items tests
jacob-a-brown Aug 14, 2025
b41a9c3
test: implement patch contact and items tests
jacob-a-brown Aug 14, 2025
452b954
feat: implement contact delete endpoints and tests
jacob-a-brown Aug 14, 2025
769d0bb
test: ensure contact id is returned in add tests
jacob-a-brown Aug 14, 2025
57eee5b
feat: add thing contact association to schemas
jacob-a-brown Aug 14, 2025
3cd96ab
feat: implement GET endpoints/tests for thing-contact association
jacob-a-brown Aug 15, 2025
2448245
fix: delete temp fixtures after yield
jacob-a-brown Aug 15, 2025
7a9d277
feat: implement PATCH endpoints/tests for thing-contact association
jacob-a-brown Aug 15, 2025
438991e
Merge branch 'jab-api-coverage-sensor' into jab-api-coverage-contact
jacob-a-brown Aug 15, 2025
c29b3f0
refactor: remove artifact from merge
jacob-a-brown Aug 15, 2025
394c2be
refactor: use pytest raises to test exceptions
jacob-a-brown Aug 15, 2025
3964f69
feat: enable POST of thing association with a contact
jacob-a-brown Aug 15, 2025
ea1b5da
refactor: refactor PydanticStyleException to enable a list of details
jacob-a-brown Aug 15, 2025
3c4eb70
fix: rollback if theres an error in creating a related record
jacob-a-brown Aug 15, 2025
e5e7983
feat: catch db errors and raise 409 error
jacob-a-brown Aug 15, 2025
6ab812f
feat: enable DELETE of thing contact association
jacob-a-brown Aug 15, 2025
a800ac4
refactor: update 409 db errors
jacob-a-brown Aug 15, 2025
9478372
Merge branch 'pre-production' into jab-api-coverage-contact
jacob-a-brown Aug 15, 2025
61d4a3b
WIP: implement contact auth and rename people helper contact helper
jacob-a-brown Aug 15, 2025
18ea5f6
refactor: put audit add in init for reuse for all helpers
jacob-a-brown Aug 15, 2025
4263cdb
refactor: put audit add in audit helper
jacob-a-brown Aug 15, 2025
81e7d47
feat: implement auth for contact router
jacob-a-brown Aug 15, 2025
9c8c4c3
fix: update test search to use fixtures
jacob-a-brown Aug 15, 2025
1df9faf
fix: fix pydantic style exception for sensor
jacob-a-brown Aug 15, 2025
2a23163
fix: fix asset schema/db/tests to correspond and use fixtures
jacob-a-brown Aug 15, 2025
4c96841
fix: fix test_search_api
jacob-a-brown Aug 15, 2025
173370a
refactor: removed print debugger
jacob-a-brown Aug 15, 2025
fcc3b35
refactor: override auth in fixture to make tests independent
jacob-a-brown Aug 18, 2025
06c28ff
Merge branch 'pre-production' into jab-api-coverage-contact
jacob-a-brown Aug 20, 2025
6578407
fix: fix artifacts/idiosyncracies from pre-production merge
jacob-a-brown Aug 20, 2025
c69934f
refactor: comment out thing-association from contact router
jacob-a-brown Aug 20, 2025
d32717a
refactor: put contact_id in payload not path POST email|phone|address
jacob-a-brown Aug 20, 2025
f747ece
fix: use passive deletes for email | phone | address
jacob-a-brown Aug 20, 2025
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
455 changes: 397 additions & 58 deletions api/contact.py

Large diffs are not rendered by default.

23 changes: 16 additions & 7 deletions api/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# limitations under the License.
# ===============================================================================

from fastapi import APIRouter, Depends, Query, HTTPException, Response
from fastapi import APIRouter, Depends, Query, Response
from sqlalchemy.exc import IntegrityError, ProgrammingError
from sqlalchemy.orm import Session
from starlette.status import HTTP_201_CREATED, HTTP_409_CONFLICT
Expand All @@ -28,6 +28,7 @@
from schemas.sample import SampleResponse, CreateSample, UpdateSample
from services.query_helper import paginated_all_getter, simple_get_by_id
from services.crud_helper import model_patcher, model_deleter
from services.exceptions_helper import PydanticStyleException

router = APIRouter(
prefix="/sample",
Expand All @@ -46,16 +47,24 @@ def database_error_handler(
error_message
== 'duplicate key value violates unique constraint "sample_field_sample_id_key"'
):
detail = (
f"Sample with field_sample_id {payload.field_sample_id} already exists."
)
detail = {
"loc": ["body", "field_sample_id"],
"msg": f"Sample with field_sample_id {payload.field_sample_id} already exists.",
"type": "value_error",
"input": {"field_sample_id": payload.field_sample_id},
}
elif (
error_message
== 'insert or update on table "sample" violates foreign key constraint "sample_thing_id_fkey"'
):
detail = f"Thing with ID {payload.thing_id} does not exist."

raise HTTPException(status_code=HTTP_409_CONFLICT, detail=detail)
detail = {
"loc": ["body", "thing_id"],
"msg": f"Thing with ID {payload.thing_id} does not exist.",
"type": "value_error",
"input": {"thing_id": payload.thing_id},
}

raise PydanticStyleException(status_code=HTTP_409_CONFLICT, detail=[detail])


# ============= Post =============================================
Expand Down
28 changes: 16 additions & 12 deletions api/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,18 @@ def update_sensor(
existing_datetime_removed is not None
and sensor_data.datetime_installed >= existing_datetime_removed
):
raise PydanticStyleException(
status_code=status.HTTP_409_CONFLICT,
loc=["body", "datetime_installed"],
msg=f"new datetime installed must be before existing datetime removed of {existing_datetime_removed.isoformat().replace('+00:00', 'Z')}",
type="value_error",
input={
detail = {
"loc": ["body", "datetime_installed"],
"msg": f"new datetime installed must be before existing datetime removed of {existing_datetime_removed.isoformat().replace('+00:00', 'Z')}",
"type": "value_error",
"input": {
"datetime_installed": sensor_data.datetime_installed.isoformat().replace(
"+00:00", "Z"
)
},
}
raise PydanticStyleException(
status_code=status.HTTP_409_CONFLICT, detail=[detail]
)
elif (
sensor_data.datetime_installed is None
Expand All @@ -81,16 +83,18 @@ def update_sensor(
sensor = simple_get_by_id(session, Sensor, sensor_id)
existing_datetime_installed = sensor.datetime_installed
if sensor_data.datetime_removed <= existing_datetime_installed:
raise PydanticStyleException(
status_code=status.HTTP_409_CONFLICT,
loc=["body", "datetime_removed"],
msg=f"new datetime removed must be after existing datetime installed of {existing_datetime_installed.isoformat().replace('+00:00', 'Z')}",
type="value_error",
input={
detail = {
"loc": ["body", "datetime_removed"],
"msg": f"new datetime removed must be after existing datetime installed of {existing_datetime_installed.isoformat().replace('+00:00', 'Z')}",
"type": "value_error",
"input": {
"datetime_removed": sensor_data.datetime_removed.isoformat().replace(
"+00:00", "Z"
)
},
}
raise PydanticStyleException(
status_code=status.HTTP_409_CONFLICT, detail=[detail]
)

return model_patcher(session, Sensor, sensor_id, sensor_data)
Expand Down
21 changes: 13 additions & 8 deletions db/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@


class ThingContactAssociation(Base, AutoBaseMixin):
thing_id = Column(Integer, ForeignKey("thing.id"), primary_key=True)
contact_id = Column(Integer, ForeignKey("contact.id"), primary_key=True)
thing_id = Column(
Integer, ForeignKey("thing.id", ondelete="CASCADE"), nullable=False
)
contact_id = Column(
Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
)

contact = relationship("Contact")
thing = relationship("Thing")
Expand All @@ -33,9 +37,9 @@ class Contact(Base, AutoBaseMixin):
name = Column(String(100), nullable=False)
role = lexicon_term(nullable=False)

phones = relationship("Phone", back_populates="contact")
emails = relationship("Email", back_populates="contact")
addresses = relationship("Address", back_populates="contact")
phones = relationship("Phone", back_populates="contact", passive_deletes=True)
emails = relationship("Email", back_populates="contact", passive_deletes=True)
addresses = relationship("Address", back_populates="contact", passive_deletes=True)

search_vector = Column(TSVectorType("name", "role"))

Expand All @@ -49,6 +53,7 @@ class Contact(Base, AutoBaseMixin):
"ThingContactAssociation",
back_populates="contact",
cascade="all, delete-orphan",
passive_deletes=True,
)
things = association_proxy("thing_associations", "thing")

Expand All @@ -60,7 +65,7 @@ class Phone(Base, AutoBaseMixin):
phone_number = Column(String(20), nullable=False)
phone_type = lexicon_term(nullable=False)

contact = relationship("Contact", back_populates="phones")
contact = relationship("Contact", back_populates="phones", passive_deletes=True)
search_vector = Column(TSVectorType("phone_number"))


Expand All @@ -71,7 +76,7 @@ class Email(Base, AutoBaseMixin):
email = Column(String(100), nullable=False)
email_type = lexicon_term(nullable=False)

contact = relationship("Contact", back_populates="emails")
contact = relationship("Contact", back_populates="emails", passive_deletes=True)

search_vector = Column(TSVectorType("email"))

Expand All @@ -88,7 +93,7 @@ class Address(Base, AutoBaseMixin):
country = lexicon_term(nullable=False, default="United States")
address_type = lexicon_term(nullable=False)

contact = relationship("Contact", back_populates="addresses")
contact = relationship("Contact", back_populates="addresses", passive_deletes=True)
search_vector = Column(
TSVectorType(
"address_line_1",
Expand Down
10 changes: 9 additions & 1 deletion db/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# ===============================================================================
from sqlalchemy import Integer, ForeignKey, String, Column, Float
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship, mapped_column, declared_attr
from sqlalchemy.orm import relationship, mapped_column
from sqlalchemy_utils import TSVectorType

from db import lexicon_term
Expand Down Expand Up @@ -45,6 +45,14 @@ class Thing(Base, AutoBaseMixin, ReleaseMixin):
)
locations = association_proxy("location_associations", "location")

contact_associations = relationship(
"ThingContactAssociation",
back_populates="thing",
overlaps="contacts",
cascade="all, delete-orphan",
)
contacts = association_proxy("contact_associations", "contact")

# Well fields
well_depth = Column(
Float,
Expand Down
Loading
Loading