"""models.petition_decisions - Data models for USPTO Final Petition Decisions API.
This module provides data models, primarily using frozen dataclasses, for
representing responses from the USPTO Final Petition Decisions API. These models
cover petition decision records, associated documents, and download options.
"""
import json
from dataclasses import dataclass, field
from datetime import date, datetime
from enum import Enum
from typing import Any
# Import parsing utilities from models utils module
from pyUSPTO.models.utils import (
parse_to_date,
parse_to_datetime_utc,
serialize_date,
serialize_datetime_as_naive,
)
# --- Enums for Categorical Data ---
[docs]
class DecisionTypeCode(Enum):
"""Represents the type of decision made on a petition.
The only current value possible is "c" which indicates DENIED.
Hopefully the USPTO will give access to others in the future.
"""
# GRANTED = "GRANTED"
DENIED = "DENIED"
# DISMISSED = "DISMISSED"
C = DENIED
@classmethod
def _missing_(cls, value: Any) -> "DecisionTypeCode":
"""Handle case-insensitive lookup and common aliases."""
if isinstance(value, str):
val_upper = value.upper()
# if val_upper == "GRANTED":
# return cls.GRANTED
if val_upper in ("DENIED", "C"):
return cls.DENIED
# if val_upper == "DISMISSED":
# return cls.DISMISSED
return super()._missing_(value=value) # type: ignore[no-any-return]
[docs]
class DocumentDirectionCategory(Enum):
"""Represents the direction of a document relative to the USPTO."""
INCOMING = "INCOMING"
OUTGOING = "OUTGOING"
@classmethod
def _missing_(cls, value: Any) -> "DocumentDirectionCategory":
"""Handle case-insensitive lookup."""
if isinstance(value, str):
val_upper = value.upper()
if val_upper == "INCOMING":
return cls.INCOMING
if val_upper == "OUTGOING":
return cls.OUTGOING
return super()._missing_(value=value) # type: ignore[no-any-return]
# --- Data Models ---
[docs]
@dataclass(frozen=True)
class DocumentDownloadOption:
"""Represent a download option for a petition decision document.
Attributes:
mime_type_identifier: The document format type (e.g., "PDF", "XML", "MS_WORD").
download_url: The URL from which the document can be downloaded.
page_total_quantity: The total number of pages in the document (if applicable).
"""
mime_type_identifier: str | None = None
download_url: str | None = None
page_total_quantity: int | None = None
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "DocumentDownloadOption":
"""Create a DocumentDownloadOption instance from a dictionary.
Args:
data: Dictionary containing download option data from API response.
Returns:
DocumentDownloadOption: An instance of DocumentDownloadOption.
"""
return cls(
mime_type_identifier=data.get("mimeTypeIdentifier"),
download_url=data.get("downloadUrl"),
page_total_quantity=data.get("pageTotalQuantity"),
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the DocumentDownloadOption instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary representation with camelCase keys.
"""
d = {
"mimeTypeIdentifier": self.mime_type_identifier,
"downloadUrl": self.download_url,
"pageTotalQuantity": self.page_total_quantity,
}
return {k: v for k, v in d.items() if v is not None}
[docs]
@dataclass(frozen=True)
class PetitionDecisionDocument:
"""Represent a document associated with a petition decision.
Attributes:
application_number_text: The application number associated with the document.
official_date: The official date of the document.
document_identifier: A unique identifier for the document.
document_code: The code identifying the document type.
document_code_description_text: Description of the document code.
direction_category: Whether the document is INCOMING or OUTGOING.
download_option_bag: List of available download options for the document.
"""
application_number_text: str | None = None
official_date: datetime | None = None
document_identifier: str | None = None
document_code: str | None = None
document_code_description_text: str | None = None
direction_category: str | None = None
download_option_bag: list[DocumentDownloadOption] = field(default_factory=list)
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "PetitionDecisionDocument":
"""Create a PetitionDecisionDocument instance from a dictionary.
Args:
data: Dictionary containing document data from API response.
Returns:
PetitionDecisionDocument: An instance of PetitionDecisionDocument.
"""
# Parse download options
download_options_data = data.get("downloadOptionBag", [])
download_options = (
[
DocumentDownloadOption.from_dict(option)
for option in download_options_data
if isinstance(option, dict)
]
if isinstance(download_options_data, list)
else []
)
return cls(
application_number_text=data.get("applicationNumberText"),
official_date=parse_to_datetime_utc(data.get("officialDate")),
document_identifier=data.get("documentIdentifier"),
document_code=data.get("documentCode"),
document_code_description_text=data.get("documentCodeDescriptionText"),
direction_category=data.get("directionCategory"),
download_option_bag=download_options,
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the PetitionDecisionDocument instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary representation with camelCase keys.
"""
d = {
"applicationNumberText": self.application_number_text,
"officialDate": (
serialize_datetime_as_naive(self.official_date)
if self.official_date
else None
),
"documentIdentifier": self.document_identifier,
"documentCode": self.document_code,
"documentCodeDescriptionText": self.document_code_description_text,
"directionCategory": self.direction_category,
"downloadOptionBag": [opt.to_dict() for opt in self.download_option_bag],
}
return {
k: v
for k, v in d.items()
if v is not None and (not isinstance(v, list) or v)
}
[docs]
@dataclass(frozen=True)
class PetitionDecision:
"""Represent a final petition decision record.
This is the main data model representing a single petition decision from the
USPTO Final Petition Decisions API. It contains comprehensive information about
the decision, including application details, decision metadata, parties involved,
and associated documents.
Attributes:
petition_decision_record_identifier: Unique identifier for the petition decision record.
application_number_text: The USPTO application number.
patent_number: The patent number if the application has been granted.
decision_date: The date the decision was submitted for issuance or mailing.
petition_mail_date: The date the decision was issued or mailed.
decision_petition_type_code: Three-digit code for the petition type.
decision_type_code: Code indicating the decision type (e.g., "C" for DENIED).
decision_type_code_description_text: Description of the decision type.
final_deciding_office_name: The USPTO office that decided the petition.
first_applicant_name: Name of the first applicant.
first_inventor_name: Name of the first inventor (computed from inventor_bag).
invention_title: Title of the invention/application.
first_inventor_to_file_indicator: Whether this is a first-inventor-to-file application.
business_entity_status_category: Entity status (e.g., "Small", "Micro", "Regular Undiscounted").
customer_number: Customer number for correspondence.
group_art_unit_number: The art unit number.
technology_center: The technology center code.
prosecution_status_code: Code for the disposition of the petition.
prosecution_status_code_description_text: Description of the prosecution status.
action_taken_by_court_name: Name of court if court action was taken.
court_action_indicator: Whether court action was taken on the decision.
inventor_bag: List of inventor names.
petition_issue_considered_text_bag: Issues under review in the petition.
statute_bag: Applicable laws under United States Code Title 35.
rule_bag: Applicable rules under Title 37 CFR.
document_bag: Associated documents for this petition decision.
last_ingestion_datetime: The last time the record was ingested/updated.
"""
petition_decision_record_identifier: str
application_number_text: str | None = None
patent_number: str | None = None
decision_date: date | None = None
petition_mail_date: date | None = None
decision_petition_type_code: int | None = None
decision_type_code: str | None = None
decision_type_code_description_text: str | None = None
final_deciding_office_name: str | None = None
first_applicant_name: str | None = None
first_inventor_name: str | None = None
invention_title: str | None = None
first_inventor_to_file_indicator: bool | None = None
business_entity_status_category: str | None = None
customer_number: int | None = None
group_art_unit_number: str | None = None
technology_center: str | None = None
prosecution_status_code: str | None = None
prosecution_status_code_description_text: str | None = None
action_taken_by_court_name: str | None = None
court_action_indicator: bool | None = None
inventor_bag: list[str] = field(default_factory=list)
petition_issue_considered_text_bag: list[str] = field(default_factory=list)
statute_bag: list[str] = field(default_factory=list)
rule_bag: list[str] = field(default_factory=list)
document_bag: list[PetitionDecisionDocument] = field(default_factory=list)
last_ingestion_datetime: datetime | None = None
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "PetitionDecision":
"""Create a PetitionDecision instance from a dictionary.
Args:
data: Dictionary containing petition decision data from API response.
Returns:
PetitionDecision: An instance of PetitionDecision.
"""
# Parse document bag
documents_data = data.get("documentBag", [])
documents = (
[
PetitionDecisionDocument.from_dict(doc)
for doc in documents_data
if isinstance(doc, dict)
]
if isinstance(documents_data, list)
else []
)
# Parse simple string lists
inventor_bag = data.get("inventorBag", [])
if not isinstance(inventor_bag, list):
inventor_bag = []
petition_issues = data.get("petitionIssueConsideredTextBag", [])
if not isinstance(petition_issues, list):
petition_issues = []
statute_bag = data.get("statuteBag", [])
if not isinstance(statute_bag, list):
statute_bag = []
rule_bag = data.get("ruleBag", [])
if not isinstance(rule_bag, list):
rule_bag = []
# Compute first inventor name from inventor bag
first_inventor_name = inventor_bag[0] if inventor_bag else None
return cls(
petition_decision_record_identifier=data.get(
"petitionDecisionRecordIdentifier", ""
),
application_number_text=data.get("applicationNumberText"),
patent_number=data.get("patentNumber"),
decision_date=parse_to_date(data.get("decisionDate")),
petition_mail_date=parse_to_date(data.get("petitionMailDate")),
decision_petition_type_code=data.get("decisionPetitionTypeCode"),
decision_type_code=data.get("decisionTypeCode"),
decision_type_code_description_text=data.get(
"decisionTypeCodeDescriptionText"
),
final_deciding_office_name=data.get("finalDecidingOfficeName"),
first_applicant_name=data.get("firstApplicantName"),
first_inventor_name=first_inventor_name,
invention_title=data.get("inventionTitle"),
first_inventor_to_file_indicator=data.get("firstInventorToFileIndicator"),
business_entity_status_category=data.get("businessEntityStatusCategory"),
customer_number=data.get("customerNumber"),
group_art_unit_number=data.get("groupArtUnitNumber"),
technology_center=data.get("technologyCenter"),
prosecution_status_code=data.get("prosecutionStatusCode"),
prosecution_status_code_description_text=data.get(
"prosecutionStatusCodeDescriptionText"
),
action_taken_by_court_name=data.get("actionTakenByCourtName"),
court_action_indicator=data.get("courtActionIndicator"),
inventor_bag=inventor_bag,
petition_issue_considered_text_bag=petition_issues,
statute_bag=statute_bag,
rule_bag=rule_bag,
document_bag=documents,
last_ingestion_datetime=parse_to_datetime_utc(
data.get("lastIngestionDateTime")
),
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the PetitionDecision instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary representation with camelCase keys.
"""
d = {
"petitionDecisionRecordIdentifier": self.petition_decision_record_identifier,
"applicationNumberText": self.application_number_text,
"patentNumber": self.patent_number,
"decisionDate": serialize_date(self.decision_date),
"petitionMailDate": serialize_date(self.petition_mail_date),
"decisionPetitionTypeCode": self.decision_petition_type_code,
"decisionTypeCode": self.decision_type_code,
"decisionTypeCodeDescriptionText": self.decision_type_code_description_text,
"finalDecidingOfficeName": self.final_deciding_office_name,
"firstApplicantName": self.first_applicant_name,
"inventionTitle": self.invention_title,
"firstInventorToFileIndicator": self.first_inventor_to_file_indicator,
"businessEntityStatusCategory": self.business_entity_status_category,
"customerNumber": self.customer_number,
"groupArtUnitNumber": self.group_art_unit_number,
"technologyCenter": self.technology_center,
"prosecutionStatusCode": self.prosecution_status_code,
"prosecutionStatusCodeDescriptionText": self.prosecution_status_code_description_text,
"actionTakenByCourtName": self.action_taken_by_court_name,
"courtActionIndicator": self.court_action_indicator,
"inventorBag": self.inventor_bag,
"petitionIssueConsideredTextBag": self.petition_issue_considered_text_bag,
"statuteBag": self.statute_bag,
"ruleBag": self.rule_bag,
"documentBag": [doc.to_dict() for doc in self.document_bag],
"lastIngestionDateTime": (
serialize_datetime_as_naive(self.last_ingestion_datetime)
if self.last_ingestion_datetime
else None
),
}
return {
k: v
for k, v in d.items()
if v is not None and (not isinstance(v, list) or v)
}
[docs]
@dataclass(frozen=True)
class PetitionDecisionResponse:
"""Response from the Final Petition Decisions API search endpoint.
This is the standard response format for search queries to the
/api/v1/petition/decisions/search endpoint and the get-by-ID endpoint.
Attributes:
count: The number of petition decisions returned in this response.
request_identifier: A unique identifier for the API request.
petition_decision_data_bag: List of petition decision records.
raw_data: Optional raw JSON data from the API response (for debugging).
"""
count: int = 0
request_identifier: str | None = None
petition_decision_data_bag: list[PetitionDecision] = field(default_factory=list)
raw_data: str | None = field(default=None, compare=False, repr=False)
[docs]
@classmethod
def from_dict(
cls, data: dict[str, Any], include_raw_data: bool = False
) -> "PetitionDecisionResponse":
"""Create a PetitionDecisionResponse instance from a dictionary.
Args:
data: Dictionary containing API response data.
include_raw_data: If True, store the raw JSON for debugging.
Returns:
PetitionDecisionResponse: An instance of PetitionDecisionResponse.
"""
# Parse petition decisions
decisions_data = data.get("petitionDecisionDataBag", [])
decisions = (
[
PetitionDecision.from_dict(decision)
for decision in decisions_data
if isinstance(decision, dict)
]
if isinstance(decisions_data, list)
else []
)
return cls(
count=data.get("count", 0),
request_identifier=data.get("requestIdentifier"),
petition_decision_data_bag=decisions,
raw_data=json.dumps(data) if include_raw_data else None,
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the PetitionDecisionResponse instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary representation with camelCase keys.
"""
d = {
"count": self.count,
"requestIdentifier": self.request_identifier,
"petitionDecisionDataBag": [
decision.to_dict() for decision in self.petition_decision_data_bag
],
}
return {
k: v
for k, v in d.items()
if v is not None and (not isinstance(v, list) or v)
}
[docs]
@dataclass(frozen=True)
class PetitionDecisionDownloadResponse:
"""Response from the Final Petition Decisions API download endpoint.
This is the response format for download queries to the
/api/v1/petition/decisions/search/download endpoint when format=json.
Note that the structure is slightly different from the search endpoint.
Attributes:
petition_decision_data: List of petition decision records.
"""
petition_decision_data: list[PetitionDecision] = field(default_factory=list)
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "PetitionDecisionDownloadResponse":
"""Create a PetitionDecisionDownloadResponse instance from a dictionary.
Args:
data: Dictionary containing download API response data.
Returns:
PetitionDecisionDownloadResponse: An instance of PetitionDecisionDownloadResponse.
"""
# Parse petition decisions (note different key name for download endpoint)
decisions_data = data.get("petitionDecisionData", [])
decisions = (
[
PetitionDecision.from_dict(decision)
for decision in decisions_data
if isinstance(decision, dict)
]
if isinstance(decisions_data, list)
else []
)
return cls(petition_decision_data=decisions)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the PetitionDecisionDownloadResponse instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary representation with camelCase keys.
"""
d = {
"petitionDecisionData": [
decision.to_dict() for decision in self.petition_decision_data
],
}
return {
k: v
for k, v in d.items()
if v is not None and (not isinstance(v, list) or v)
}