"""models.oa_rejections - Data models for USPTO Office Action Rejections API.
This module provides data models for representing responses from the USPTO
Office Action Rejections API (v2). These models cover rejection-level data
from Office Actions including rejection type indicators, claim arrays, and
examiner classification metadata.
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from pyUSPTO.models.utils import parse_to_datetime_utc, serialize_datetime_as_naive
[docs]
@dataclass(frozen=True)
class OARejectionsRecord:
"""A single rejection record from the OA Rejections API.
Attributes:
id: Unique record identifier (hex hash).
patent_application_number: USPTO patent application number.
legacy_document_code_identifier: Document code (e.g., ``"CTNF"``, ``"NOA"``).
action_type_category: Type of office action (e.g., ``"rejected"``).
legal_section_code: Legal provision under which the action was taken.
group_art_unit_number: Examiner group art unit (e.g., ``"1713"``).
national_class: USPC national class code.
national_subclass: USPC national subclass code.
paragraph_number: Paragraph number referenced in the action.
obsolete_document_identifier: Legacy IFW document identifier.
create_user_identifier: Job identifier that inserted this record.
claim_number_array_document: Claim numbers referenced in this record,
split from the API's comma-separated string format.
submission_date: Date the office action was submitted.
create_date_time: Timestamp when this record was inserted into the database.
has_rej_101: Whether a 35 U.S.C. § 101 rejection was raised.
has_rej_102: Whether a 35 U.S.C. § 102 rejection was raised.
has_rej_103: Whether a 35 U.S.C. § 103 rejection was raised.
has_rej_112: Whether a 35 U.S.C. § 112 rejection was raised.
has_rej_dp: Whether a non-statutory double patenting rejection was raised.
cite_103_max: Largest number of references in any single § 103 rejection.
cite_103_eq1: Whether exactly one reference was cited in a § 103 rejection.
cite_103_gt3: Whether more than three references were cited in a § 103 rejection.
closing_missing: Whether the closing paragraph is missing from the action.
reject_form_missmatch: Whether the form content doesn't match the document code.
Note: field name preserves the API's original spelling.
form_paragraph_missing: Whether a required form paragraph is missing.
header_missing: Whether the standard metadata header is missing.
bilski_indicator: Whether the Bilski v. Kappos decision is referenced.
mayo_indicator: Whether the Mayo v. Prometheus decision is referenced.
alice_indicator: Whether the Alice/Mayo framework is applied for § 101 review.
myriad_indicator: Whether the Myriad Genetics decision is applied.
allowed_claim_indicator: Whether the application contains allowed claims.
"""
id: str = ""
patent_application_number: str | None = None
legacy_document_code_identifier: str | None = None
action_type_category: str | None = None
legal_section_code: str | None = None
group_art_unit_number: str | None = None
national_class: str | None = None
national_subclass: str | None = None
paragraph_number: str | None = None
obsolete_document_identifier: str | None = None
create_user_identifier: str | None = None
claim_number_array_document: list[str] = field(default_factory=list)
submission_date: datetime | None = None
create_date_time: datetime | None = None
has_rej_101: bool | None = None
has_rej_102: bool | None = None
has_rej_103: bool | None = None
has_rej_112: bool | None = None
has_rej_dp: bool | None = None
cite_103_max: int | None = None
cite_103_eq1: int | None = None
cite_103_gt3: int | None = None
closing_missing: int | None = None
reject_form_missmatch: int | None = None
form_paragraph_missing: int | None = None
header_missing: int | None = None
bilski_indicator: bool | None = None
mayo_indicator: bool | None = None
alice_indicator: bool | None = None
myriad_indicator: bool | None = None
allowed_claim_indicator: bool | None = None
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "OARejectionsRecord":
"""Create an OARejectionsRecord from a dictionary.
Args:
data: Dictionary containing rejection record data from API response.
Returns:
OARejectionsRecord: An instance of OARejectionsRecord.
"""
def _get_bool(key: str) -> bool | None:
val = data.get(key)
if val is None:
return None
return bool(val)
def _get_int(key: str) -> int | None:
val = data.get(key)
if val is None:
return None
return int(val)
# Split comma-separated claim numbers from the list-of-strings API format
raw_claims = data.get("claimNumberArrayDocument", [])
if not isinstance(raw_claims, list):
raw_claims = []
claim_number_array_document: list[str] = []
for item in raw_claims:
if isinstance(item, str):
claim_number_array_document.extend(
s.strip() for s in item.split(",") if s.strip()
)
return cls(
id=data.get("id", ""),
patent_application_number=data.get("patentApplicationNumber"),
legacy_document_code_identifier=data.get("legacyDocumentCodeIdentifier"),
action_type_category=data.get("actionTypeCategory"),
legal_section_code=data.get("legalSectionCode"),
group_art_unit_number=data.get("groupArtUnitNumber"),
national_class=data.get("nationalClass"),
national_subclass=data.get("nationalSubclass"),
paragraph_number=data.get("paragraphNumber"),
obsolete_document_identifier=data.get("obsoleteDocumentIdentifier"),
create_user_identifier=data.get("createUserIdentifier"),
claim_number_array_document=claim_number_array_document,
submission_date=parse_to_datetime_utc(data.get("submissionDate")),
create_date_time=parse_to_datetime_utc(data.get("createDateTime")),
has_rej_101=_get_bool("hasRej101"),
has_rej_102=_get_bool("hasRej102"),
has_rej_103=_get_bool("hasRej103"),
has_rej_112=_get_bool("hasRej112"),
has_rej_dp=_get_bool("hasRejDP"),
cite_103_max=_get_int("cite103Max"),
cite_103_eq1=_get_int("cite103EQ1"),
cite_103_gt3=_get_int("cite103GT3"),
closing_missing=_get_int("closingMissing"),
reject_form_missmatch=_get_int("rejectFormMissmatch"),
form_paragraph_missing=_get_int("formParagraphMissing"),
header_missing=_get_int("headerMissing"),
bilski_indicator=_get_bool("bilskiIndicator"),
mayo_indicator=_get_bool("mayoIndicator"),
alice_indicator=_get_bool("aliceIndicator"),
myriad_indicator=_get_bool("myriadIndicator"),
allowed_claim_indicator=_get_bool("allowedClaimIndicator"),
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the OARejectionsRecord instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary with camelCase keys matching the API format.
Claim numbers are joined back to a comma-separated string in a list.
None values and empty lists are omitted.
"""
claims_serialized = (
[",".join(self.claim_number_array_document)]
if self.claim_number_array_document
else []
)
d: dict[str, Any] = {
"id": self.id,
"patentApplicationNumber": self.patent_application_number,
"legacyDocumentCodeIdentifier": self.legacy_document_code_identifier,
"actionTypeCategory": self.action_type_category,
"legalSectionCode": self.legal_section_code,
"groupArtUnitNumber": self.group_art_unit_number,
"nationalClass": self.national_class,
"nationalSubclass": self.national_subclass,
"paragraphNumber": self.paragraph_number,
"obsoleteDocumentIdentifier": self.obsolete_document_identifier,
"createUserIdentifier": self.create_user_identifier,
"claimNumberArrayDocument": claims_serialized,
"submissionDate": (
serialize_datetime_as_naive(self.submission_date)
if self.submission_date
else None
),
"createDateTime": (
serialize_datetime_as_naive(self.create_date_time)
if self.create_date_time
else None
),
"hasRej101": self.has_rej_101,
"hasRej102": self.has_rej_102,
"hasRej103": self.has_rej_103,
"hasRej112": self.has_rej_112,
"hasRejDP": self.has_rej_dp,
"cite103Max": self.cite_103_max,
"cite103EQ1": self.cite_103_eq1,
"cite103GT3": self.cite_103_gt3,
"closingMissing": self.closing_missing,
"rejectFormMissmatch": self.reject_form_missmatch,
"formParagraphMissing": self.form_paragraph_missing,
"headerMissing": self.header_missing,
"bilskiIndicator": self.bilski_indicator,
"mayoIndicator": self.mayo_indicator,
"aliceIndicator": self.alice_indicator,
"myriadIndicator": self.myriad_indicator,
"allowedClaimIndicator": self.allowed_claim_indicator,
}
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 OARejectionsResponse:
"""Response from the OA Rejections API search endpoint.
The API returns a Solr-style response with ``start``, ``numFound``, and ``docs``.
The outer envelope key is ``"response"``.
Attributes:
num_found: Total number of matching records.
start: The start index of the first result in this page.
docs: List of rejection records in this page.
raw_data: Optional raw JSON data from the API response (for debugging).
"""
num_found: int = 0
start: int = 0
docs: list[OARejectionsRecord] = field(default_factory=list)
raw_data: str | None = field(default=None, compare=False, repr=False)
@property
def count(self) -> int:
"""Return total result count for pagination compatibility."""
return self.num_found
[docs]
@classmethod
def from_dict(
cls, data: dict[str, Any], include_raw_data: bool = False
) -> "OARejectionsResponse":
"""Create an OARejectionsResponse from a dictionary.
Handles both the raw API envelope (``{"response": {...}}``) and
a pre-unwrapped dictionary.
Args:
data: Dictionary containing API response data.
include_raw_data: If True, store the raw JSON for debugging.
Returns:
OARejectionsResponse: An instance of OARejectionsResponse.
"""
inner = data.get("response", data)
docs_data = inner.get("docs", [])
docs = (
[
OARejectionsRecord.from_dict(doc)
for doc in docs_data
if isinstance(doc, dict)
]
if isinstance(docs_data, list)
else []
)
return cls(
num_found=inner.get("numFound", 0),
start=inner.get("start", 0),
docs=docs,
raw_data=json.dumps(data) if include_raw_data else None,
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the OARejectionsResponse instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary wrapped in the ``"response"`` envelope
matching the API format.
"""
return {
"response": {
"numFound": self.num_found,
"start": self.start,
"docs": [doc.to_dict() for doc in self.docs],
}
}
[docs]
@dataclass(frozen=True)
class OARejectionsFieldsResponse:
"""Response from the OA Rejections API fields endpoint.
Contains metadata about the API including available field names
and the last data update timestamp.
Attributes:
api_key: The dataset key (e.g., ``"oa_rejections"``).
api_version_number: API version (e.g., ``"v2"``).
api_url: The URL of this fields endpoint.
api_documentation_url: URL to the Swagger documentation.
api_status: Publication status (e.g., ``"PUBLISHED"``).
field_count: Number of available fields.
fields: List of available field names.
last_data_updated_date: Timestamp of the last data update (non-standard format).
"""
api_key: str | None = None
api_version_number: str | None = None
api_url: str | None = None
api_documentation_url: str | None = None
api_status: str | None = None
field_count: int = 0
fields: list[str] = field(default_factory=list)
last_data_updated_date: str | None = None
[docs]
@classmethod
def from_dict(
cls, data: dict[str, Any], include_raw_data: bool = False
) -> "OARejectionsFieldsResponse":
"""Create an OARejectionsFieldsResponse from a dictionary.
Args:
data: Dictionary containing API response data.
include_raw_data: Unused. Present for FromDictProtocol conformance.
Returns:
OARejectionsFieldsResponse: An instance of OARejectionsFieldsResponse.
"""
fields_data = data.get("fields", [])
if not isinstance(fields_data, list):
fields_data = []
return cls(
api_key=data.get("apiKey"),
api_version_number=data.get("apiVersionNumber"),
api_url=data.get("apiUrl"),
api_documentation_url=data.get("apiDocumentationUrl"),
api_status=data.get("apiStatus"),
field_count=data.get("fieldCount", 0),
fields=fields_data,
last_data_updated_date=data.get("lastDataUpdatedDate"),
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Convert the OARejectionsFieldsResponse instance to a dictionary.
Returns:
Dict[str, Any]: Dictionary representation with camelCase keys.
"""
d: dict[str, Any] = {
"apiKey": self.api_key,
"apiVersionNumber": self.api_version_number,
"apiUrl": self.api_url,
"apiDocumentationUrl": self.api_documentation_url,
"apiStatus": self.api_status,
"fieldCount": self.field_count,
"fields": self.fields,
"lastDataUpdatedDate": self.last_data_updated_date,
}
return {
k: v
for k, v in d.items()
if v is not None and (not isinstance(v, list) or v)
}