Skip to content
Merged
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Releases are also tagged in git, if that's helpful.
The following changes are not yet released, but are code complete:

Features:
-
- Add Trademark Trial and Appeal Board scraper #1851

Changes:
-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
"mspb_p",
"mspb_u",
"olc",
"ttab",
]
99 changes: 99 additions & 0 deletions juriscraper/opinions/united_states/administrative_agency/ttab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Scraper for Trademark Trial and Appeal Board (TTAB) Reading Room
CourtID: ttab
Court Short Name: TTAB
Author: Ansel Halliburton
Type: Precedential
"""

import json
from datetime import date, datetime

from juriscraper.AbstractSite import logger
from juriscraper.OpinionSiteLinear import OpinionSiteLinear


class Site(OpinionSiteLinear):
first_opinion_date = datetime(1986, 12, 23)
days_interval = 365
TTAB_RR_BASE = "https://ttab-reading-room.uspto.gov"
TTAB_RR_API = f"{TTAB_RR_BASE}/ttab-efoia-api/decision/search"
TTAB_RR_PDF_BASE = f"{TTAB_RR_BASE}/cms/rest"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.court_id = self.__module__
self.url = self.TTAB_RR_API
self.method = "POST"
self.status = "Published"
self.should_have_results = True
self._search_payload = {
"dateRangeData": {},
"facetData": {},
"parameterData": {"precedentCitableIndicator": "Y"},
"searchText": "",
"sortDataBag": [{"issueDate": "desc"}],
"recordStartNumber": 0,
}
self.make_backscrape_iterable(kwargs)

def _download(self, request_dict=None):
if self.test_mode_enabled():
return super()._download(request_dict)
self.downloader_executed = True
response = self.request["session"].post(
self.TTAB_RR_API,
json=self._search_payload,
headers=self.request["headers"],
verify=self.request["verify"],
)
response.raise_for_status()
self.request["response"] = response
return response.json()

def _process_html(self):
if self.test_mode_enabled():
with open(self.url) as f:
data = json.load(f)
else:
data = self.html

results = data.get("results", [])
seen_docs = set()
for r in results:
doc_id = r.get("documentId", "")
if not doc_id or doc_id in seen_docs:
continue
seen_docs.add(doc_id)
self.cases.append(
{
"name": r.get("partyName", "").strip(),
"url": f"{self.TTAB_RR_PDF_BASE}{doc_id}",
"date": r.get("issueDateStr", ""),
"docket": r.get("proceedingNumberDisplay", ""),
"judge": r.get("panelMember", "").replace(";", ", "),
"disposition": r.get("decision", ""),
"summary": r.get("issue", ""),
"status": "Published",
}
)

def _download_backwards(self, dates: tuple[date, date]) -> None:
logger.info("Backscraping for range %s %s", *dates)
self._search_payload["dateRangeData"] = {
"decisionDate": {
"from": dates[0].strftime("%Y-%m-%d"),
"to": dates[1].strftime("%Y-%m-%d"),
}
}
self._search_payload["recordStartNumber"] = 0
self.html = self._download()
total = self.html.get("recordTotalQuantity", 0)
self._process_html()

page_size = 25
start = page_size
while start < total:
self._search_payload["recordStartNumber"] = start
self.html = self._download()
self._process_html()
start += page_size
28 changes: 28 additions & 0 deletions tests/examples/opinions/united_states/ttab_example.compare.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[
{
"case_dates": "2026-01-27",
"case_names": "ACME Corp.",
"download_urls": "https://ttab-reading-room.uspto.gov/cms/rest/ttab-documents/2026/01/123456.pdf",
"precedential_statuses": "Published",
"blocked_statuses": false,
"date_filed_is_approximate": false,
"dispositions": "Sustained",
"docket_numbers": "91234567",
"judges": "Judge Smith, Judge Jones, Judge Brown",
"summaries": "Likelihood of confusion",
"case_name_shorts": "ACME Corp."
},
{
"case_dates": "2025-11-15",
"case_names": "Widgets Inc.",
"download_urls": "https://ttab-reading-room.uspto.gov/cms/rest/ttab-documents/2025/11/789012.pdf",
"precedential_statuses": "Published",
"blocked_statuses": false,
"date_filed_is_approximate": false,
"dispositions": "Dismissed",
"docket_numbers": "91234568",
"judges": "Judge Adams, Judge Baker",
"summaries": "Descriptiveness",
"case_name_shorts": "Widgets Inc."
}
]
29 changes: 29 additions & 0 deletions tests/examples/opinions/united_states/ttab_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"recordTotalQuantity": 2,
"results": [
{
"id": 12345,
"documentId": "/ttab-documents/2026/01/123456.pdf",
"partyName": "ACME Corp.",
"proceedingNumberDisplay": "91234567",
"issueDateStr": "27-JAN-2026",
"issueDate": 1738022400000,
"decision": "Sustained",
"panelMember": "Judge Smith;Judge Jones;Judge Brown",
"issue": "Likelihood of confusion",
"precedentCitableIndicator": "Y"
},
{
"id": 12346,
"documentId": "/ttab-documents/2025/11/789012.pdf",
"partyName": "Widgets Inc.",
"proceedingNumberDisplay": "91234568",
"issueDateStr": "15-NOV-2025",
"issueDate": 1731628800000,
"decision": "Dismissed",
"panelMember": "Judge Adams;Judge Baker",
"issue": "Descriptiveness",
"precedentCitableIndicator": "Y"
}
]
}
Loading