Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
46d7081
feat(lib): Typehints for `time_call` decorator
MorganBennetDev May 19, 2026
eb253d4
refactor(texas): Move MergeResult and docket disaggregation
MorganBennetDev May 19, 2026
686e722
feat(florida): Docket merger
MorganBennetDev May 19, 2026
d6a4b8c
test(florida): Factories
MorganBennetDev May 19, 2026
5716940
test(florida): Docket and OCI merger tests
MorganBennetDev May 19, 2026
3a3f7c6
feat(florida): DN core extraction
MorganBennetDev May 20, 2026
3e6478a
feat(florida): Update find_docket_object with Florida DN core
MorganBennetDev May 20, 2026
d4314dc
chore(florida): Preemptively narrow atomic block in Florida docket me…
MorganBennetDev May 20, 2026
df851e6
Merge branch 'main' into morgan/7361-florida-docket-merger
MorganBennetDev May 26, 2026
e02e086
feat(corpusimporter): Merger abstraction draft
MorganBennetDev Jun 1, 2026
66031be
feat(corpusimporter): Merger abstraction cleanup
MorganBennetDev Jun 1, 2026
2127551
test(corpusimporter): Add tests for merger abstraction
MorganBennetDev Jun 1, 2026
64aa594
feat(corpusimporter): Related object mergers
MorganBennetDev Jun 2, 2026
fc0002d
test(corpusimporter): Test abstract related mergers
MorganBennetDev Jun 2, 2026
1b5cb44
test(corpusimporter): Test abstract merger inheritance
MorganBennetDev Jun 2, 2026
1683ccc
docs(corpusimporter): Abstract merger documentation
MorganBennetDev Jun 2, 2026
18df01e
fix(corpusimporter): Harden merger abstraction from review
MorganBennetDev Jun 3, 2026
181bd30
fix(corpusimporter): Satisfy mypy on the merger abstraction
MorganBennetDev Jun 3, 2026
85bd3bf
refactor(corpusimporter): Port Florida merger to the abstraction
MorganBennetDev Jun 3, 2026
3dec12e
feat(corpus-importer): Switch abstract merger off of Annotated becaus…
MorganBennetDev Jun 3, 2026
6d94c73
feat(corpus-importer): Switch Florida merger off Annotated style
MorganBennetDev Jun 3, 2026
d8284eb
fix(corpus_importer): Satisfy mypy on the state merger
MorganBennetDev Jun 3, 2026
fb92877
feat(florida): Add appeal from to merger
MorganBennetDev Jun 3, 2026
83104e6
fix(texas): Forgot to rename variable
MorganBennetDev Jun 3, 2026
d1a0736
fix(corpus-importer): mypy gripes on test file
MorganBennetDev Jun 3, 2026
b1c0f67
fix(types): mypy is very confused about the concept of union types
MorganBennetDev Jun 3, 2026
dd9c453
Merge branch 'main' into morgan/7361-florida-docket-merger
albertisfu Jun 9, 2026
557a379
fix(florida): Apply suggestions from code review
MorganBennetDev Jun 10, 2026
aa71a92
fix(florida): Address review comments
MorganBennetDev Jun 10, 2026
24124a3
Merge branch 'main' into morgan/7361-florida-docket-merger
MorganBennetDev Jun 10, 2026
2487b86
fix(florida): Guard empty originating cases and restore use-first OCI…
MorganBennetDev Jun 10, 2026
e8f12a2
Merge remote-tracking branch 'origin/morgan/7361-florida-docket-merge…
MorganBennetDev Jun 10, 2026
07ad840
chore(florida): Source `FLORIDA_COURT_IDS` from `FLORIDA_COURT_ID_MAP`
MorganBennetDev Jun 10, 2026
c2906d9
fix(florida): Address review
MorganBennetDev Jun 16, 2026
6bea3bd
refactor(florida): Remove model introspection
MorganBennetDev Jun 16, 2026
d862131
refactor(florida): Switch transforms and strategies to functions; sim…
MorganBennetDev Jun 16, 2026
c732800
fix(merger): mypy
MorganBennetDev Jun 17, 2026
8128c5b
refactor(florida): Use managers for better child merging; combine met…
MorganBennetDev Jun 17, 2026
54b5150
refactor(florida): Remove `RelationshipType`
MorganBennetDev Jun 17, 2026
b27985a
feat(merger): Add validation to `__init_subclass__`
MorganBennetDev Jun 17, 2026
8437d5c
refactor(merger): Bind merger names via __set_name__
MorganBennetDev Jun 17, 2026
ff0a52b
chore(merger): Cleanup
MorganBennetDev Jun 17, 2026
f333efb
Merge branch 'main' into morgan/7361-florida-docket-merger
MorganBennetDev Jun 22, 2026
eb136c9
refactor(merger): Better class heirarchy
MorganBennetDev Jun 22, 2026
8c28bca
refactor(merger): Get rid of unnecessary court disaggregation method
MorganBennetDev Jun 26, 2026
16b7303
fix(florida): A new test along with some fixes
MorganBennetDev Jun 26, 2026
7df13ff
fix(merger): Get rid of `Any` subclassing
MorganBennetDev Jun 30, 2026
4d5ee58
refactor(merger): Move validation and registration out of Merger
MorganBennetDev Jun 30, 2026
73c2a4f
refactor(merger): Per-relation-type mergers
MorganBennetDev Jun 30, 2026
c6014bf
refactor(merger): Better parameter passing
MorganBennetDev Jun 30, 2026
d9b8c22
refactor(merger): Collapse spec generics
MorganBennetDev Jun 30, 2026
43827ec
fix(merger): Repair transform type inference in factories
MorganBennetDev Jun 30, 2026
68b0f98
fix(merger): Better type hints
MorganBennetDev Jun 30, 2026
446f58b
refactor(merger): Make merge an instance operation
MorganBennetDev Jun 30, 2026
b61d121
Merge branch 'main' into morgan/7361-florida-docket-merger
albertisfu Jul 2, 2026
0ae68ec
fix(merger): Florida DNs and merger diagnostics
MorganBennetDev Jul 2, 2026
171a8c2
chore(merger): Remove stray comment
MorganBennetDev Jul 2, 2026
62bda29
Merge branch 'main' into morgan/7361-florida-docket-merger
MorganBennetDev Jul 6, 2026
92c1946
fix(florida): Add tests for DN core and correct failing test
MorganBennetDev Jul 9, 2026
3bda8ad
feat(florida): Use same logic as find_docket_object in merger
MorganBennetDev Jul 9, 2026
4499e3a
fix(recap): Bad conditional in find_docket_object
MorganBennetDev Jul 9, 2026
4fb2ae4
Merge branch 'main' into morgan/7361-florida-docket-merger
albertisfu Jul 10, 2026
ec22316
fix(florida): Address review feedback
MorganBennetDev Jul 13, 2026
5fcdff7
perf(recap): Remove redundant count call
MorganBennetDev Jul 13, 2026
a81d79b
fix(recap): Get rid of additional query
MorganBennetDev Jul 14, 2026
c5b22fc
refactor(recap): Eliminate another query and make a (potential) optim…
MorganBennetDev Jul 14, 2026
62da576
Merge branch 'main' into morgan/7361-florida-docket-merger
albertisfu Jul 15, 2026
1e9ff9b
docs(recap): Document reason for slicing
MorganBennetDev Jul 16, 2026
d14643a
Merge branch 'main' into morgan/7361-florida-docket-merger
albertisfu Jul 17, 2026
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
Empty file.
100 changes: 100 additions & 0 deletions cl/corpus_importer/state/florida/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Factories for mocking output of Florida Juriscraper modules."""

from factory.base import Factory
from factory.declarations import LazyAttribute, List, SubFactory
from factory.faker import Faker
from juriscraper.state.docket import DocketTransfer
from juriscraper.state.florida.cases import (
FLORIDA_DOCKET_TYPE_MAP,
FloridaCase,
FloridaOriginatingCase,
)
from juriscraper.state.florida.courts import FloridaCourtID
from juriscraper.state.florida.docket_entries import FloridaDocketEntry
from juriscraper.state.florida.parties import FloridaParty


class _PydanticConstructFactory(Factory):
"""Builds Pydantic models via ``model_construct`` so factories can use
field names directly even though the upstream models declare
``validation_alias``\\es matching the Florida API payload shape."""

class Meta:
abstract = True

@classmethod
def _build(cls, model_class, *args, **kwargs):
return model_class.model_construct(**kwargs)

@classmethod
def _create(cls, model_class, *args, **kwargs):
return model_class.model_construct(**kwargs)


class FloridaOriginatingCaseFactory(_PydanticConstructFactory):
class Meta:
model = FloridaOriginatingCase

court_name = Faker("court_name")
court_id = Faker(
"random_element",
elements=(FloridaCourtID.CIRCUIT, FloridaCourtID.COUNTY),
)
case_number = Faker("federal_district_docket_number")


# The merger does not currently read fields off these objects, so the factories
# just produce stubs.
class FloridaDocketTransferFactory(_PydanticConstructFactory):
class Meta:
model = DocketTransfer


class FloridaCasePartyFactory(_PydanticConstructFactory):
class Meta:
model = FloridaParty


class FloridaDocketEntryFactory(_PydanticConstructFactory):
class Meta:
model = FloridaDocketEntry

date_filed = Faker("date_object")


class FloridaCaseFactory(_PydanticConstructFactory):
class Meta:
model = FloridaCase

case_uuid = Faker("uuid4")
docket_number = Faker("federal_district_docket_number")
case_name = Faker("case_name")
case_name_full = Faker("case_name", full=True)
case_caption = Faker("text")
closed_flag = Faker("pybool")
class_group_type = Faker("pystr")
class_group_type_id = Faker("pyint")
docket_type = Faker(
"random_element", elements=list(FLORIDA_DOCKET_TYPE_MAP.values())
)
classification_id = Faker("pyint")
court_id = Faker(
"random_element",
elements=(
FloridaCourtID.FIRST_COA.value,
FloridaCourtID.SECOND_COA.value,
FloridaCourtID.SIXTH_COA.value,
FloridaCourtID.SUPREME_COURT.value,
),
)
court_abbreviation = Faker("pystr", max_chars=3)
location = Faker("city")
location_id = Faker("pyint")
datetime_filed = Faker("date_time")
date_filed = LazyAttribute(lambda o: o.datetime_filed.date())
Comment thread
MorganBennetDev marked this conversation as resolved.
case_group_flag = Faker("pybool")
panel_flag = Faker("pybool")
originating_cases = List([SubFactory(FloridaOriginatingCaseFactory)])
transfers = List([SubFactory(FloridaDocketTransferFactory)])
entries = List([SubFactory(FloridaDocketEntryFactory)])
parties = List([SubFactory(FloridaCasePartyFactory)])
192 changes: 192 additions & 0 deletions cl/corpus_importer/state/florida/mergers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import logging
from datetime import date
from typing import ClassVar, override

from asgiref.sync import async_to_sync
from django.db.models import Model, QuerySet
from juriscraper.state.florida import FloridaCase, FloridaOriginatingCase
from juriscraper.state.florida.cases import FloridaCourtID

from cl.corpus_importer.state.florida.utils import (
FL_APPELLATE_COURT_ID,
FLORIDA_COURT_ID_MAP,
make_docket_number_core,
)
from cl.corpus_importer.state.merger import (
Attribute,
Merger,
OneToOneRelation,
overwrite,
)
from cl.recap.mergers import find_docket_object_query
from cl.search.models import Docket, OriginatingCourtInformation

logger = logging.getLogger(__name__)


def add_scraper_source(scrape: int | None, db: int | None) -> int:
if not db:
db = 0
if db in Docket.NON_SCRAPER_SOURCES():
return db + Docket.SCRAPER
return db


def _date_last_filing(docket_data: FloridaCase, params: None) -> date | None:
filing_dates = sorted(
e.date_filed for e in docket_data.entries if e.date_filed
)
return filing_dates[-1] if filing_dates else docket_data.date_filed


def _appeal_from_id(docket_data: FloridaCase, params: None) -> str | None:
# Multiple originating cases are ambiguous, so leave the field unset.
if len(docket_data.originating_cases) != 1:
return None
return FLORIDA_COURT_ID_MAP.get(
docket_data.originating_cases[0].court_id.value, None
)


def _appeal_from_str(docket_data: FloridaCase, params: None) -> str | None:
# Multiple originating cases are ambiguous, so leave the field unset.
if len(docket_data.originating_cases) != 1:
return ""
return docket_data.originating_cases[0].court_name


class FloridaOriginatingCourtInformationMerger(
Merger[FloridaOriginatingCase, None, OriginatingCourtInformation]
):
model: ClassVar[type[Model]] = OriginatingCourtInformation

docket_number: str = Attribute(
lambda oc, params: oc.case_number, strategy=overwrite
)
docket_number_raw: str = Attribute(
lambda oc, params: oc.case_number, strategy=overwrite
)

def query(self) -> QuerySet[OriginatingCourtInformation]:
return OriginatingCourtInformation.objects.none()


def _originating_case(
docket_data: FloridaCase, params: None
) -> FloridaOriginatingCase | None:
if docket_data.court_id != FloridaCourtID.SUPREME_COURT.value:
return None
if not docket_data.originating_cases:
return None
if len(docket_data.originating_cases) > 1:
logger.warning(
"Florida docket %s in court %s has multiple originating cases. Using the first one.",
docket_data.docket_number,
docket_data.court_id,
)
return docket_data.originating_cases[0]


class FloridaDocketMerger(Merger[FloridaCase, None, Docket]):
model: ClassVar[type[Model]] = Docket

atomic = True

court_id: str = Attribute(
lambda d, params: FLORIDA_COURT_ID_MAP[d.court_id],
strategy=overwrite,
)
source: int = Attribute(
lambda _, params: Docket.SCRAPER,
strategy=add_scraper_source,
)
date_filed: date | None = Attribute(
lambda d, params: d.date_filed,
strategy=overwrite,
)
date_last_filing: date | None = Attribute(
_date_last_filing,
strategy=overwrite,
)
case_name: str = Attribute(
lambda d, params: d.case_name, strategy=overwrite
)
case_name_full: str = Attribute(
lambda d, params: d.case_name_full,
strategy=overwrite,
)
case_name_short: str = Attribute(
lambda d, params: d.case_name, strategy=overwrite
)
docket_number: str = Attribute(
lambda d, params: d.docket_number,
strategy=overwrite,
)
docket_number_raw: str = Attribute(
lambda d, params: d.docket_number, strategy=overwrite
)
docket_number_core: str = Attribute(
lambda d, params: make_docket_number_core(
d.docket_number, court_id=FLORIDA_COURT_ID_MAP[d.court_id]
),
strategy=overwrite,
)
appeal_from_id: str | None = Attribute(_appeal_from_id, strategy=overwrite)
appeal_from_str: str | None = Attribute(
_appeal_from_str, strategy=overwrite
)
# See https://github.com/freelawproject/courtlistener/issues/7361#issuecomment-4566459292
pacer_case_id: str = Attribute(
lambda d, params: str(d.case_uuid), strategy=overwrite
)
originating_court_information: OriginatingCourtInformation = (
OneToOneRelation(
FloridaOriginatingCourtInformationMerger,
_originating_case,
)
)

@override
def query(self) -> QuerySet[Docket]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing this and found that two cases with the same UUID but different docket numbers are not matching. We should prioritize the UUID (pacer_case_id) as the primary source of truth when matching existing cases, even if the docket number has changed (for example, due to a correction from the court).

We could use logic similar to find_docket_object, where pacer_case_id is always prioritized, either in combination with the docket number or on its own.

Why wasn't it possible to continue using find_docket_object here? It would be great to keep the matching logic in a single place if possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wasn't it possible to continue using find_docket_object here? It would be great to keep the matching logic in a single place if possible.

When I wrote this, I didn't want to update find_docket_object to be able to return a query set, but I should probably just do that. Maybe something like:

  • Create find_docket_object_query which will house the for kwargs in lookups loop from find_docket_object, returning a queryset, and
  • Update find_docket_object to only do the DN core validation and checks, leaving the actual lookup logic in find_docket_object_query.

Then Florida and future Merger subclasses can use find_docket_object_query to keep the same logic as find_docket_object (minus the DN core conversion, which they should handle themselves).

What do you think?

supreme_court_id = FLORIDA_COURT_ID_MAP[
FloridaCourtID.SUPREME_COURT.value
]
court_id = FLORIDA_COURT_ID_MAP[self.scrape.court_id]
dn_core = make_docket_number_core(
self.scrape.docket_number, court_id=court_id
)

query_narrow = async_to_sync(find_docket_object_query)(
court_id=court_id,
pacer_case_id=str(self.scrape.case_uuid),
docket_number=self.scrape.docket_number,
docket_number_core=dn_core,
federal_defendant_number=None,
federal_dn_judge_initials_assigned=None,
federal_dn_judge_initials_referred=None,
skip_dn_core_confirmation=True,
)

if court_id == supreme_court_id:
return query_narrow

if query_narrow.count() == 0:
return async_to_sync(find_docket_object_query)(
court_id=FL_APPELLATE_COURT_ID,
pacer_case_id=str(self.scrape.case_uuid),
docket_number=self.scrape.docket_number,
docket_number_core=dn_core,
federal_defendant_number=None,
federal_dn_judge_initials_assigned=None,
federal_dn_judge_initials_referred=None,
skip_dn_core_confirmation=True,
)

return query_narrow

@staticmethod
def validate(scrape: FloridaCase) -> bool:
if scrape.court_id not in FLORIDA_COURT_ID_MAP:
logger.error("Unknown court id: %s", scrape.court_id)
return False
return True
Loading
Loading