From 50343bd2db1e4df2c76089b91b82bf36766ab5ff Mon Sep 17 00:00:00 2001 From: Rob Alexander <6756310+ralexx@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:35:24 -0700 Subject: [PATCH 1/3] First draft of 197-scotus-scraper --- juriscraper/dockets/__init__.py | 0 juriscraper/dockets/united_states/__init__.py | 0 .../federal_appellate/__init__.py | 0 .../federal_appellate/scotus/__init__.py | 0 .../federal_appellate/scotus/clients.py | 296 +++++ .../federal_appellate/scotus/docket_search.py | 711 +++++++++++ .../federal_appellate/scotus/scotus_docket.py | 123 ++ .../federal_appellate/scotus/utils.py | 207 ++++ juriscraper/lib/exceptions.py | 29 + requirements.txt | 1 + tests/__init__.py | 8 +- .../scotus/SCOTUS_home_20240304.html | 1078 +++++++++++++++++ .../scotus/scotus_23-175.json | 1 + .../scotus/scotus_JSON_not_found.html | 506 ++++++++ .../scotus/scotus_access_denied.html | 13 + .../scotus/scotus_docket_search_home.aspx | 584 +++++++++ .../scotus_docket_search_results_page1.aspx | 614 ++++++++++ .../scotus_docket_search_results_page2.aspx | 48 + .../scotus/scotus_granted_noted.html | 636 ++++++++++ .../scotus/scotus_journal.html | 560 +++++++++ .../scotus/scotus_orders_home.html | 966 +++++++++++++++ tests/local/test_ScotusClientTest.py | 239 ++++ tests/local/test_ScotusDocketSearchTest.py | 242 ++++ tests/network/test_ScotusClientTest.py | 178 +++ 24 files changed, 7039 insertions(+), 1 deletion(-) create mode 100644 juriscraper/dockets/__init__.py create mode 100644 juriscraper/dockets/united_states/__init__.py create mode 100644 juriscraper/dockets/united_states/federal_appellate/__init__.py create mode 100644 juriscraper/dockets/united_states/federal_appellate/scotus/__init__.py create mode 100644 juriscraper/dockets/united_states/federal_appellate/scotus/clients.py create mode 100644 juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py create mode 100644 juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py create mode 100644 juriscraper/dockets/united_states/federal_appellate/scotus/utils.py create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/SCOTUS_home_20240304.html create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_23-175.json create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_JSON_not_found.html create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_access_denied.html create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page1.aspx create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page2.aspx create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_granted_noted.html create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_journal.html create mode 100644 tests/examples/dockets/united_states/federal_appellate/scotus/scotus_orders_home.html create mode 100644 tests/local/test_ScotusClientTest.py create mode 100644 tests/local/test_ScotusDocketSearchTest.py create mode 100644 tests/network/test_ScotusClientTest.py diff --git a/juriscraper/dockets/__init__.py b/juriscraper/dockets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/juriscraper/dockets/united_states/__init__.py b/juriscraper/dockets/united_states/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/juriscraper/dockets/united_states/federal_appellate/__init__.py b/juriscraper/dockets/united_states/federal_appellate/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/__init__.py b/juriscraper/dockets/united_states/federal_appellate/scotus/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py b/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py new file mode 100644 index 000000000..59c63491a --- /dev/null +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py @@ -0,0 +1,296 @@ +"""Download clients for supremecourt.gov""" + +from random import random, choices +import re + +from lxml import html +from lxml.etree import ParserError +import requests +from requests.exceptions import ConnectionError +from urllib3.exceptions import NameResolutionError + +from juriscraper.lib.log_tools import make_default_logger +from juriscraper.lib.exceptions import AccessDeniedError +from . import utils + + +logger = make_default_logger() + +AGENTS = [ + { + "ua": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 " + "(KHTML, like Gecko) Version/17.3.1 Safari/605.1.1" + ), + "pct": 30.0, + }, + { + "ua": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.3" + ), + "pct": 28.39, + }, + { + "ua": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 " + "(KHTML, like Gecko) Version/16.6 Safari/605.1.1" + ), + "pct": 24.84, + }, + { + "ua": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0." + ), + "pct": 7.74, + }, + { + "ua": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 " + "Firefox/117." + ), + "pct": 2.58, + }, + { + "ua": ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.3" + ), + "pct": 1.29, + }, + { + "ua": ( + "Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.3" + ), + "pct": 1.29, + }, + { + "ua": ( + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 " + "Firefox/115." + ), + "pct": 1.29, + }, + { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.", + "pct": 1.29, + }, + { + "ua": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.3" + ), + "pct": 1.29, + }, +] + +HEADERS = { + "Accept": ( + "text/html,application/xhtml+xml,application/xml;q=0.9," + "image/avif,image/webp,*/*;q=0.8" + ), + "Cache-Control": "no-cache", + "DNT": "1", + "Host": "www.supremecourt.gov", + "Pragma": "no-cache", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Sec-GPC": "1", + "Upgrade-Insecure-Requests": "1", + # "User-Agent": "Juriscraper", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.36" + ), +} + + +not_found_regex = re.compile(r"(?<=ERROR).*not found") + + +def _access_denied_test(text: str) -> bool: + """Take an HTML string from a `Response.text` and test for + `Access Denied`.""" + try: + root = html.document_fromstring(text) + status = root.head.text_content().strip() == "Access Denied" + except (ParserError, IndexError): + return False + else: + return status + + +def is_access_denied_page(response: requests.Response) -> bool: + """Take an HTML string from a `Response.text` and test for + `Access Denied`.""" + ct, cl = [ + response.headers.get(f) for f in ("content-type", "content-length") + ] + if ct and cl: + if ct.startswith("text/html") and int(cl) > 0: + return _access_denied_test(response.text) + return False + + +def _not_found_test(text: str) -> bool: + """Take an HTML string from a `Response.text` and test for + `Access Denied`.""" + try: + root = html.document_fromstring(text) + error_string = root.get_element_by_id("pagetitle").text_content() + except (ParserError, KeyError): + return False + else: + if not_found_regex.search(error_string): + return True + else: + return False + + +def is_not_found_page(response: requests.Response) -> bool: + """Take an HTML string from a `Response.text` and test for + `ERROR: File or directory not found.`. + """ + ct, cl = [ + response.headers.get(f) for f in ("content-type", "content-length") + ] + if ct and cl: + if ct.startswith("text/html") and int(cl) > 0: + return _not_found_test(response.text) + return False + + +def is_docket(response: requests.Response) -> bool: + """Handle two valid possibilities: docket number returns status code 200 and + either exists or returns HTML error page.""" + if not isinstance(response, requests.Response): + raise TypeError( + f"Expected requests.Response (sub)class; got {type(response)}" + ) + + is_json = "application/json" in response.headers.get("content-type", "") + return response.status_code == 200 and is_json + + +def is_stale_content(response: requests.Response) -> bool: + """Check for response status code 304 but this can include unused + docket numbers where the Not Found page has a Last-Modified header.""" + return response.status_code == 304 and len(response.content) == 0 + + +def jitter(scale: float = 0.15) -> float: + """Return a random decimal fraction multiplied by `scale`. Add this to + retry delays so they space out (i.e. increase dispersion).""" + return random() * scale + + +def random_ua() -> str: + """Return a randomly selected User Agent string.""" + ua_weights = [u["pct"] for u in AGENTS] + return choices(AGENTS, cum_weights=ua_weights)[0]["ua"] + + +def download_client( + url: str, + *, + since_timestamp: str = None, + session: requests.Session = None, + **kwargs, +) -> requests.Response: + """Wrapper for requests.Session that can add the 'If-Modified-Since' header + to requests so server can return status code 304 ('Not Modified') for stale pages. + + :param url: URL to be requested. + :param since_timestamp: Exclude search results modified before this date. + Input 'YYYY-MM-DD' format for good results. Download requests use the + 'If-Modified-Since' header to skip source documents that have not been + updated since `since_timestamp`. + :param retries: Maximum number of retries to accommodate transient problems. + :param retry_increment: Additional sleep time added for each retry. + :return: A requests.Response object. + + Note: kwargs passed to Session query methods (.get, .post, etc). + + The host appears to have consistent support for the + 'Last-Modified' header. When a timestamp string (see formatting below) + is passed as the value of the 'If-Modified-Since' request header, + assets whose 'Last-Modified' header is earlier that 'If-Modified-Since' + will return status code 304 before terminating the response without + downloading any further content. This can save significant time + when executing multiple requests. + + Some drawbacks to using this approach: + * Requests to non-existent docket pages intermittently return an 'Access Denied' + text/html page -- possibly when the request rate from a client IP address is + too high. This response type does not contain a 'Last-Modified' header. + * does not appear to return 404 Not Found codes for dockets + (and all other asserts I have worked with so far). Instead, a 'Not Found' + HTML page is returned: either with status code 200, or with status code + 304 if the 'Last-Modified' date of that 'Not Found' page is less than + the 'If-Modified-Since' argument! This behavior makes the use of + 'If-Modified-Since' unsuitable for discovering newly populated docket + numbers. + """ + if not session: + _session = requests.Session() + else: + _session = session + + _session.headers.update(HEADERS | {"User-Agent": random_ua()}) + # allow 304 response codes for pages updated before `since_timestamp` + ts_format = "%a, %d %b %Y %H:%M:%S GMT" + if since_timestamp: + mod_stamp = utils.makedate(since_timestamp) + mod_header = {"If-Modified-Since": mod_stamp.strftime(ts_format)} + _session.headers.update(mod_header) + + logger.debug(f"Querying {url}") + try: + response = _session.get(url, **kwargs) + return response + finally: + if not session: + # only close the session created here + _session.close() + + +def response_handler(response: requests.Response) -> requests.Response: + """If download is not valid, determine appropriate exception to raise so + caller can respond e.g. by retrying or stalling.""" + + logger.debug(f"Handling Response <{response.url}>") + try: + response.raise_for_status() + except ConnectionError as ce: + if ( + isinstance(ce.__context__, NameResolutionError) + or isinstance(ce.__cause__, NameResolutionError) + or "NameResolutionError" in repr(ce) + ): + # server-side measures; throttle and retry + logger.error( + f"Server-side measures: {response.url} raised {repr(ce)}", + exc_info=ce, + ) + raise + except Exception as e: + logger.error(f"UNCAUGHT: {response.url} raised {repr(e)}", exc_info=e) + logger.debug(response.headers) + logger.debug(f"Response().text: {response.text}") + raise + else: + if is_access_denied_page(response): + # let caller log and handle + logger.error( + f"Server-side measures: {response.url} returned 'Access Denied' page" + ) + logger.debug(response.headers) + logger.debug(f"Response().text: {response.text}") + raise AccessDeniedError(f"{response.url}") + else: + return response diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py b/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py new file mode 100644 index 000000000..e90b5764e --- /dev/null +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py @@ -0,0 +1,711 @@ +"""Resources for scraping SCOTUS docket numbers from court documents. + +> Paper remains the official form of filing at the Supreme Court. + + +Perhaps because of this second-millennium record keeping practice, there is no +publicly-available digital central index of Supreme Court cases. If SCOTUS docket +information is not obtained by attempting to query docket numbers in sequence, +one of the Court's information sources needs to be consulted. + +In descending order of relevance to scraping, these are: + +[Orders of the Court](https://www.supremecourt.gov/orders/ordersofthecourt/) +[Docket Search](https://www.supremecourt.gov/docket/docket.aspx) +[Journal of the SCOTUS](https://www.supremecourt.gov/orders/journal.aspx) + +**Orders of the Court** +* PDFs listing docket numbers of cases for which there has been an action. +> Regularly scheduled lists of orders are issued on each Monday that the Court +sits, but 'miscellaneous' orders may be issued in individual cases at any time. +Scheduled order lists are posted on this Website on the day of their issuance, +while miscellaneous orders are posted on the day of issuance or the next day." + +**Docket Search** +* Full-text search portal that attempts to match the input string to any text +in any docket. +* Returns URLs of the HTML rendering for dockets. From these, docket numbers can +be parsed and substituted into the URL pattern for dockets' JSON rendering. +* Passing date strings formatted like docket entry dates (e.g. 'Feb 29, 2024') +will with high probability return a link to a docket with an entry(s) matching +the input string. +* There appears to be a limit of 500 search results. Because some of the results +will be for matches in the text of a docket entry rather than the entry date +itself, it is possible that some search results will not be exhaustive. + +**Journal of the Supreme Court** +* The exhaustive -- but not at all timely -- PDFs containing all docket numbers. +> The Journal of the Supreme Court of the United States contains the official +minutes of the Court. It is published chronologically for each day the Court +issues orders or opinions or holds oral argument. The Journal reflects the +disposition of each case, names the court whose judgment is under review, lists +the cases argued that day and the attorneys who presented oral argument[]. +""" +from datetime import date, timedelta +from io import BytesIO + +from random import shuffle +import re +from time import sleep + +import fitz # the package name of the `pymupdf` library +from lxml import html +import requests + +from juriscraper.lib.log_tools import make_default_logger +from .clients import ( + download_client, + response_handler, + is_not_found_page, + is_stale_content, + jitter, + HEADERS, + random_ua, +) +from . import utils + +logger = make_default_logger() + +order_url_date_regex = re.compile(r"(\d{2})(\d{2})(\d{2})(zr|zor)") +fedrules_regex = re.compile(r"(?<=\/)(?<=\/)fr\w\w\d\d") + + +class SCOTUSOrders: + """Download and parse 'Orders of the Court' PDF documents for a single term. + + These Orders include the following types of court action: + + * Certiorari -- Summary Dispositions + * Certiorari Granted + * Certiorari Denied + * Statements of Justices [accompanying denial/grant of certiorari] + + The Orders *do not* include dispositions for which an Opinion is published. + + We ignore Federal Rules of judicial procedure (28 USC §2072) amendments that + are periocially released as Orders but are not relevant here. These PDFs + follow a file naming convention that begins with 'fr', unlike case-related + Orders whose filenames start with six characters for MMDDYY and then + either 'zor' for a regular Orders List or 'zr' for a Miscellaneous Order. + """ + + def __init__( + self, + term: int, + *, + session: requests.Session = None, + cache_pdfs: bool = True, + **kwargs, + ): + """Will instantiate a Session if none is passed.""" + self.term = term + self._session = session + self.homepage_response = None + self.cache_pdfs = cache_pdfs + self.order_meta = None + self._pdf_cache = set() + self._docket_numbers = set() + + @property + def session(self) -> requests.Session: + if self._session is None: + self._session = requests.Session() + self._session.headers.update(HEADERS) + logger.debug(f"{self} instantiated new Session") + return self._session + + def __del__(self): + if self._session: + try: + self._session.close() + finally: + pass + + def orders_page_download( + self, + since_timestamp: str = None, + **kwargs, + ) -> None: + """Download the Orders page from a single US Supreme Court term. + + Note: + kwargs passed to session.get(). + """ + if isinstance(self.term, int): + yy_term = str(self.term)[-2:] + url = f"https://www.supremecourt.gov/orders/ordersofthecourt/{yy_term}" + + response = download_client( + url=url, + since_timestamp=since_timestamp, + **kwargs, + ) + # allow 304 reponses; those will be managed + self.homepage_response = response_handler(response) + + @staticmethod + def orders_link_parser(html_string: str) -> list: + """Take an HTML string from the Orders page and return a list of Order URLs.""" + root = html.document_fromstring(html_string) + url_xpath = '//*[@id="pagemaindiv"]//a[contains(@href, "courtorders")]' + pdf_link_elements = root.xpath(url_xpath) + + link_matches = [] + for x in pdf_link_elements: + url = f"https://www.supremecourt.gov{x.get('href')}" + # order_type = x.text + link_matches.append(url) + return link_matches + + def parse_orders_page(self) -> None: + """Extract URLs for individual order documents. Append to self.order_meta + with Order date and order type of 'zor' (order list) or 'zr' (misc. order). + """ + if self.homepage_response is None: + self.orders_page_download() + + logger.debug(f"Parsing orders page for {self.term} term...") + try: + order_urls = self.orders_link_parser(self.homepage_response.text) + except Exception as e: + logger.error( + f"orders_link_parser {self.term} raised {repr(e)}", exc_info=e + ) + raise + else: + logger.info( + f"Order PDF count for term {self.term}: {len(order_urls)}" + ) + + # extract metadata for each order list from the URL + self.order_meta = [] + + for url in order_urls: + container = {} + try: + _mm, _dd, _yy, listype = order_url_date_regex.search( + url + ).groups() + except AttributeError as ae: + if fedrules_regex.search(url): + # Order is an update to the federal judicial rules; ignore + logger.debug(f"Ignoring judicial rules update {url}") + else: + logger.error( + f"order_url_date_regex {self.term} failed on {url}", + exc_info=ae, + ) + continue + except TypeError as e2: + logger.error( + f"URL parsing error {self.term} on {url}", exc_info=e2 + ) + continue + else: + container["order_date"] = date( + int("20" + _yy), int(_mm), int(_dd) + ) + container["order_type"] = listype + container["url"] = url + self.order_meta.append(container) + + def order_pdf_download( + self, + url: str, + since_timestamp: str = None, + **kwargs, + ) -> requests.Response: + """Download an Orders PDF. + + Note: kwargs passed to session.get(). + """ + pdf_headers = { + "Accept": ( + "text/html,application/xhtml+xml,application/xml;q=0.9," + "image/avif,image/webp,*/*;q=0.8" + ), + "Host": "www.supremecourt.gov", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Sec-GPC": "1", + } + + response = download_client( + url=url, + since_timestamp=since_timestamp, + headers=pdf_headers, + **kwargs, + ) + # allow caller to handle status code 304 responses + return response_handler(response) + + @staticmethod + def order_pdf_parser(pdf: bytes) -> set: + """Extract docket numbers from an Orders PDF. In-memory objects are passed to + the 'stream' argument.""" + if isinstance(pdf, BytesIO): + open_kwds = dict(stream=pdf) + elif isinstance(pdf, bytes): + open_kwds = dict(stream=BytesIO(pdf)) + else: + open_kwds = dict(filename=pdf) + + _pdf = fitz.open(**open_kwds) + pdf_string = "\n".join([pg.get_text() for pg in _pdf]) + matches = utils.orders_docket_regex.findall(pdf_string) + # clean up dash characters so only U+002D is used + docket_set = set( + [utils.dash_regex.sub("\u002d", dn) for dn in matches] + ) + return docket_set + + def _term_constraints(self, *, earliest: str, latest: str) -> dict: + """Allow for more targeted Order searches within a single SCOTUS term. + Terms run October-September.""" + + earliest = utils.makedate(earliest).date() if earliest else None + latest = utils.makedate(latest).date() if latest else None + term_beg = utils.first_monday_oct(self.term) + term_end = utils.first_monday_oct(self.term + 1) - timedelta(days=1) + + # handle earliest-latest transposition + if (earliest and latest) and (earliest > latest): + logger.debug( + f"`earliest` {earliest} must be less than `latest` {latest}; " + "switching their input order" + ) + _start = earliest + _stop = latest + else: + # searches run in descending chronological order + _stop = earliest + _start = latest + + # apply constraints + if _start: + _start = min(_start, term_end) + if _stop: + _stop = max(_stop, term_beg) + return {"earliest": _stop, "latest": _start} + + def docket_numbers( + self, + earliest: date = None, + latest: date = None, + include_misc: bool = True, + **kwargs, + ) -> list: + """Return a sorted list representation of the scraped docket number set.""" + if self._docket_numbers == set(): + self._get_orders( + earliest=earliest, latest=latest, include_misc=include_misc + ) + return sorted(self._docket_numbers, key=utils.docket_priority) + + def _get_orders( + self, + earliest: date, + latest: date, + include_misc: bool, + delay: float = 0.1, + **kwargs, + ) -> None: + """Find available Orders published for a given court term, extract their + metadata and save to database. + + :param earliest: Date object or string for the earliest order date to download. + :param latest: Date object or string for the most recent order date to download. + :param include_misc: False to exclude miscellaneous orders; this feature + intended for debugging use, e.g. to validate free text searches against a + specific order list. + """ + available_terms = set(range(2016, utils.current_term() + 1)) + if self.term not in available_terms: + errmsg = "Only these terms available for scraping: {}" + logger.error( + f"Bad term {self.term} passed to orders_scraping_manager" + ) + raise ValueError(errmsg.format(available_terms)) + + # contstrain search dates to the indicated term + date_constraints = self._term_constraints( + earliest=earliest, latest=latest + ) + earliest = date_constraints["earliest"] + latest = date_constraints["latest"] + logger.debug( + "Scraping docket numbers from Orders: " + f"{dict(latest=latest, earliest=earliest, misc_orders=include_misc)}" + ) + + # find Orders for this term + self.orders_page_download(**kwargs) + + # extract Orders PDF URLs + self.parse_orders_page() + + # download PDFs + for meta_record in self.order_meta: + # skip Miscellaneous Orders if `include_misc` is False + if not include_misc and meta_record["order_type"] == "zr": + continue + + # enforce date constraints if they were passed + if (earliest and meta_record["order_date"] < earliest) or ( + latest and meta_record["order_date"] > latest + ): + continue + + url = meta_record["url"] + pdf_response = self.order_pdf_download(url, **kwargs) + + if is_stale_content(pdf_response): + # Got status code 304 + logger.debug(f"{url} returned status code 304; skipping ahead") + continue + else: + if self.cache_pdfs: + self._pdf_cache.add(pdf_response) + + # parse out docket numbers + if is_not_found_page(pdf_response): + logger.info(f"{url} is stale; skipping") + continue + + try: + docket_numbers = self.order_pdf_parser( + BytesIO(pdf_response.content) + ) + except Exception as e: + if "code=2: no objects found" in repr(e): + # bad URL; log and skip + logger.info(f"{url} is stale; skipping") + else: + logger.error( + f"PDF parser error {self.term} on {url}", + exc_info=e, + ) + logger.debug(str(pdf_response.request.headers)) + logger.debug(str(pdf_response.headers)) + logger.debug(str(pdf_response.content[:100])) + if self.cache_pdfs: + continue + else: + raise + else: + # perform set update + self._docket_numbers.update(docket_numbers) + logger.info( + f'Scraped {len(docket_numbers)} dockets from {meta_record["order_date"]} {meta_record["order_type"]}' + ) + sleep(delay + jitter(delay)) + + +class DocketFullTextSearch: + """Wrapper for SCOTUS 'Docket Search' feature. Search on a single string. + """ + + SEARCH_URL = "https://www.supremecourt.gov/docket/docket.aspx" + + static_headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "www.supremecourt.gov", + "Origin": "https://www.supremecourt.gov", + "Referer": "https://www.supremecourt.gov/docket/docket.aspx", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-User": "?1", + "Sec-GPC": "1", + "TE": "trailers", + "User-Agent": random_ua(), + } + + post_template = { + "ctl00_ctl00_RadScriptManager1_TSM": "", + "__EVENTTARGET": "", + "__EVENTARGUMENT": "", + "__VIEWSTATE": None, + "__VIEWSTATEGENERATOR": None, + "ctl00$ctl00$txtSearch": "", + "ctl00$ctl00$txbhidden": "", + "ct": "Supreme-Court-Dockets", + "ctl00$ctl00$MainEditable$mainContent$txtQuery": None, + "ctl00$ctl00$MainEditable$mainContent$cmdSearch": "Search", + } + + viewstate_regex = re.compile(r"(?<=__VIEWSTATE\|).*?(?=\|)") + pagination_regex = re.compile(r"(\d+?)(?= items).*?(\d+) of (\d+)") + result_count_regex = re.compile(r"\d+?(?= items)") + + def __init__(self, search_string: str, session=None, delay=0.5): + """Although any search string can be passed, it will not be validated. + Use the `date_query` classmethod to instantiate this class as intended. + """ + self.search_string = search_string + self.session = session or requests.Session() + self.delay = delay + self.search_home = None + self.query_responses = [] + self._matching_dockets = set() + + # set default request headers + self.session.headers.update(self.static_headers) + + def __del__(self): + if self.session: + self.session.close() + + @classmethod + def date_query(cls, date_string, **kwargs): + """Take a parsable date string (e.g. YYYY-MM-DD) and instantiate with a string + representation that will match docket entry dates in free-text search. + """ + output_fmt = "%b %d, %Y" + date_obj = utils.makedate(date_string) + date_string = date_obj.strftime(output_fmt) + return cls(search_string=date_string, **kwargs) + + @staticmethod + def search_page_metadata_parser(html_string): + """Take an HTML string from the docket search web page and return + metadata required to perform searches. + """ + root = html.document_fromstring(html_string) + + hiddens = './/div[@class="aspNetHidden"]' + meta_elements = { + tag.name: tag.value for el in root.findall(hiddens) for tag in el + } + return meta_elements + + @staticmethod + def _viewstate_parser(html_string): + """Take an HTML string from the docket search web page and return + metadata required to perform searches. + """ + root = html.document_fromstring(html_string) + + viewstate = './/*[@id="__VIEWSTATE"]' + meta_elements = {tag.name: tag.value for tag in root.xpath(viewstate)} + return meta_elements + + def page_count_parser(self, html_string): + """Take an HTML string from the docket search results. Return + current and max `page` numbers; if missing, page count is 1. + """ + root = html.document_fromstring(html_string) + + page_row = ( + './/*[@id="ctl00_ctl00_MainEditable_mainContent_lblCurrentPage"]' + ) + element_text = root.xpath(page_row)[0].text + + # test for a single result page + if page_match := self.pagination_regex.search(element_text): + page_vals = page_match.groups() + elif page_match := self.result_count_regex.search(element_text): + page_vals = (page_match.group(0), 1, 1) + else: + raise ValueError(f"page_count_parser did not match {element_text}") + return { + k: int(v) for k, v in zip(("items", "cur_pg", "max_pg"), page_vals) + } + + def full_text_search_page(self, **kwargs): + """Download to scrape + hidden metadata. All `kwargs` are passed to the requests client. + """ + try: + response = download_client( + self.SEARCH_URL, session=self.session, **kwargs + ) + except Exception: + raise + else: + return response + + def search_query(self): + """Send POST full text search query.""" + home_page_response = self.full_text_search_page() + hidden_payload = self.search_page_metadata_parser( + home_page_response.text + ) + _search_string = self.search_string # .replace(" ", "+") + + payload = self.post_template | hidden_payload + payload["ctl00$ctl00$MainEditable$mainContent$txtQuery"] = ( + _search_string + ) + + try: + _response = self.session.post(self.SEARCH_URL, data=payload) + response = response_handler(_response) + except Exception: + raise + else: + if self.query_responses != []: + self.query_responses.clear() + self.query_responses.append(response) + + def pager(self): + """Download 2,...,n pages of search results""" + assert ( + len(self.query_responses) > 0 + ), "`search_query` must have been run first." + + header_updates = { + "Accept": "*/*", + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "X-MicrosoftAjax": "Delta=true", + "X-Requested-With": "XMLHttpRequest", + } + payload_updates = { + "ctl00$ctl00$RadScriptManager1": ( + "ctl00$ctl00$MainEditable$mainContent$UpdatePanel1" + "|ctl00$ctl00$MainEditable$mainContent$cmdNext" + ), + "__EVENTTARGET": "ctl00$ctl00$MainEditable$mainContent$cmdNext", + "__EVENTARGUMENT": "", + "ctl00$ctl00$MainEditable$mainContent$txtQuery": self.search_string, + "__ASYNCPOST": True, + "": "", + } + hidden_payload = self.search_page_metadata_parser( + self.query_responses[0].text + ) + + payload = self.post_template.copy() + # must delete the following key for paging to work + # https://toddhayton.com/2015/05/04/scraping-aspnet-pages-with-ajax-pagination/ + del payload["ctl00$ctl00$MainEditable$mainContent$cmdSearch"] + + # now update POST payload + payload.update(hidden_payload) + payload.update(payload_updates) + payload["ctl00$ctl00$MainEditable$mainContent$txtQuery"] = ( + self.search_string + ) + # extract __VIEWSTATE value from non-HTML text + if vs_match := self.viewstate_regex.search( + self.query_responses[-1].text + ): + payload["__VIEWSTATE"] = vs_match.group(0) + + try: + response = self.session.post( + self.SEARCH_URL, data=payload, headers=header_updates + ) + except Exception: + raise + else: + self.query_responses.append(response) + + def scrape(self): + """Execute a search query and page through the results.""" + self.search_query() + + try: + result_params = self.page_count_parser( + self.query_responses[0].text + ) + except AttributeError as ae: + if "NoneType" in repr(ae): + errmsg = ( + "Failed to find page count response in search on " + f"{self.search_string}" + ) + logger.error(errmsg, exc_info=ae) + logger.debug( + f"Payload: {self.query_responses[0].request.body}" + ) + raise + except Exception as e: + errmsg = f".scrape on {self.search_string} raised {repr(e)}" + logger.error(errmsg, exc_info=e) + logger.debug(self.query_responses[0].request.headers) + logger.debug(self.query_responses[0].headers) + logger.debug(f"Payload: {self.query_responses[0].request.body}") + logger.debug(f".text: {self.query_responses[0].text[:300]}") + logger.debug(self.query_responses[0].text[3000:5000]) + raise + + logger.debug( + f"'{self.search_string}' first page results {result_params}" + ) + iter_count = result_params["max_pg"] + current_page = result_params["cur_pg"] + + while current_page < iter_count: + sleep(self.delay) + try: + self.pager() + except Exception as e: + logger.error(f"current page {current_page}", exc_info=e) + raise + + try: + result_params = self.page_count_parser( + self.query_responses[-1].text + ) + except IndexError as ie: + logger.debug(self.query_responses[-1].request.headers) + logger.debug(self.query_responses[-1].headers) + logger.debug(self.query_responses[-1].text) + logger.error( + f"Current pg: {current_page} iter_count: {iter_count}", + exc_info=ie, + ) + except Exception as e: + logger.error(f"current page {current_page}", exc_info=e) + raise + else: + current_page = result_params["cur_pg"] + + @staticmethod + def docket_number_parser(html_string): + """Take an HTML string from the docket search results and return + a list of docket numbers. + """ + root = html.document_fromstring(html_string) + url_xpath = '//a[contains(@href, "docketfiles")]' + pdf_link_elements = root.xpath(url_xpath) + + link_matches = [] + for x in pdf_link_elements: + if matched := utils.docket_number_regex.search(x.text): + corrected = utils.dash_regex.sub("\u002d", matched.group(0)) + link_matches.append(utils.endocket(corrected)) + return set(link_matches) + + def parse_dockets(self): + """Parse docket numbers out of search results.""" + assert self.query_responses != [] + + for r in self.query_responses: + try: + docketnums = self.docket_number_parser(r.text) + except Exception as e: + logger.debug(r.text[5000:7000], exc_info=e) + raise + else: + self._matching_dockets.update(docketnums) + + @property + def matching_dockets(self): + """Return a sorted list of docket numbers.""" + if not self._matching_dockets: + self.scrape() + self.parse_dockets() + return sorted(self._matching_dockets, key=utils.docket_priority) + + def randomized_dockets(self): + """Return a randomized list of discovered docket numbers.""" + sd = self.matching_dockets + shuffle(sd) + return sd diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py b/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py new file mode 100644 index 000000000..62223ab65 --- /dev/null +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py @@ -0,0 +1,123 @@ +"""Downloading and parsing of dockets from the Supreme Court. + +Dockets offering a JSON rendering use the following URL naming pattern: + +Base URL: https://www.supremecourt.gov/RSS/Cases/JSON/< YY >< DT >< # >.json +Term (YY): Two-digit year of the Supreme Court term beginning in October. +Docket type (DT): One of {'-', 'A', 'M', 'O'}, corresponding to the nature of the + docket. + '-': Petitions, typically for writs of certiorari or mandamus + 'A': Applications, not yet reviewed by the Court + 'M': Motions, such as for leave to file as a veteran + 'O': 'Orig.' cases; this designation is unclear and there are few +Case number (#): Two increasing ranges of integers. + 'Paid' cases number from 1 to 4999 in a given term + 'IFP' (in forma pauperis) a.k.a pauper cases number 5001 and up each term +""" + +# from concurrent.futures import ThreadPoolExecutor, as_completed +from math import sqrt +from time import sleep + +import requests +from requests.exceptions import ConnectionError + +from juriscraper.lib.log_tools import make_default_logger +from juriscraper.lib.exceptions import AccessDeniedError +from .clients import ( + download_client, + response_handler, + jitter, + is_docket, + is_not_found_page, + is_stale_content, +) +from . import utils + + +logger = make_default_logger() + + +def linear_download( + docket_numbers: list, + delay: float = 0.25, + since_timestamp: str = None, + fails_allowed: int = 5000, + **kwargs, +): + """Iterate over specific docket numbers and yield valid responses. + + :param docket_numbers: List of docket numbers in valid format. + :param delay: Float value of throttling delay between download attempts. + """ + base_url = "https://www.supremecourt.gov/RSS/Cases/JSON/{}.json" + session = requests.Session() + + # store docket not found instances to estimate data endpoint + not_found = set() + stale_set = set() + fresh_set = set() + + # truncate possible values for rate-limiting delay + trunc_delay = max(0, min(delay, 2.0)) + + logger.info("Querying docket numbers...") + for dn in docket_numbers: + docketnum = utils.endocket(dn) + logger.debug(f"Trying docket {docketnum}...") + + # exception handling delegated to downloader + response = download_client( + url=base_url.format(docketnum), + session=session, + since_timestamp=since_timestamp, + ) + try: + valid_response = response_handler(response) + except (AccessDeniedError, ConnectionError) as e: + logger.critical(f"Abording download at {docketnum}", exc_info=e) + raise + + if is_stale_content(valid_response): + logger.debug(f"{docketnum} returned 304; skipping.") + stale_set.add(docketnum) + continue # no delay + elif is_docket(valid_response): + logger.debug(f"Found docket {docketnum}.") + fresh_set.add( + ( + docketnum, + utils.makedate(response.headers.get("Last-Modified")), + ) + ) + yield response + # delay to rate-limit requests + sleep(trunc_delay + jitter(sqrt(delay))) + elif is_not_found_page(valid_response): + not_found.add(docketnum) + logger.debug(f"Not found {docketnum}") + if len(not_found) > fails_allowed: + # stop downloading when cumulative `fails_allowed` is exceeded + break + else: + # delay to rate-limit requests + sleep(trunc_delay + jitter(sqrt(delay))) + else: + raise RuntimeError(f"Found edge case downloading {docketnum}") + + session.close() + + # log download results + logger.info( + f"Finished updating --> {{Updated: {len(fresh_set)}, " + f"Stale (ignored): {len(stale_set)}, " + f"'Not Found': {len(not_found)}}}" + ) + _fs = sorted(list(fresh_set), key=lambda z: utils.docket_priority(z[0])) + logger.debug(f"Updated dockets: {_fs}") + + _nf = sorted(list(not_found), key=utils.docket_priority) + logger.debug(f"Not Found: {_nf}") + + _ss = sorted(list(stale_set), key=utils.docket_priority) + logger.debug(f"Stale dockets: {_ss}") diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py b/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py new file mode 100644 index 000000000..b4513c2a5 --- /dev/null +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py @@ -0,0 +1,207 @@ +"""Utilities for SCOTUS docket scraping and processing.""" + +from datetime import date, datetime, timedelta +from hashlib import sha1 +import re + +import dateutil.parser + + +"""Text utilities + +Note: PDF parsing can return various Unicode dashes beyond the standard +hyphen-minus character (U+002d). The docket search pattern uses U+002d, +U+2010, U+2011, U+2012, U+2013, and U+2014. +""" + +# parse docket numbers +dash_pat = r"[\u002d\u2010\u2011\u2012\u2013\u2014]" +dash_regex = re.compile(dash_pat) +docket_pat = r"\d{2}[\u002d\u2010\u2011\u2012\u2013\u2014AaMmOo]\d{1,5}" +docket_number_regex = re.compile(docket_pat) +docket_num_strict_regex = re.compile(r"(\d\d)([-AOM])(\d{1,5})") + +# parse docket PDF filing URLs +filing_url_regex = re.compile( + ( + r"(?<=www\.supremecourt\.gov)(?:/\w+?/)" + r"(?P\d\d)/" + r"(?P\d{2}[-AMO]\d{1,5})/" + r"(?P\d+)/" + r"(?P\d{17})" + ) +) + + +# special treatment for Orders PDFs +_orders_pat = ( + r"(?<=No\. )\d{2}[\u002d\u2010\u2011\u2012\u2013\u2014AaMmOo]\d{1,5}(?=\.)" + r"|(?<=\n)\d{2}[\u002d\u2010\u2011\u2012\u2013\u2014AaMmOo]\d{1,5}" + r"|(?<=\()\d{2}[\u002d\u2010\u2011\u2012\u2013\u2014AaMmOo]\d{1,5}(?=\))" +) +orders_docket_regex = re.compile(_orders_pat) + +"""multiple dispatch docket number formatters""" + + +def dedocket(docket_number: str) -> tuple: + """Accept a padded docket string and return components of a docket number + as a tuple e.g. (2023, '-', 5) or (2017, 'A', 54).""" + term, mod, casenum = [ + docket_number[i:j] for i, j in ((0, 2), (2, 3), (3, 8)) + ] + return int("20" + term), mod.upper(), int(casenum) + + +def padocket(x) -> str: + """Accept unpadded docket number string or a tuple of docket number components + and return padded docket string formatted as e.g. '23-00005' or '17A00054'. + """ + if isinstance(x, (list, tuple)): + assert len(x) == 3 + yyyy, docket_type, case_num = x + assert ( + isinstance(yyyy, int) + and yyyy >= 2007 + and docket_type.upper() in {"-", "A", "M", "O"} + and isinstance(case_num, int) + ) + return str(yyyy)[2:4] + docket_type + ("0000" + str(case_num))[-5:] + elif isinstance(x, str): + assert docket_number_regex.search(x) + return x[:3] + ("0000" + str(int(x[3:])))[-5:] + + +def endocket(x) -> str: + """Accept padded docket number string or a tuple of docket number components + and return unpadded docket string formatted as e.g. '23-5' or '17A54'.""" + if isinstance(x, (list, tuple)): + assert len(x) == 3 + yyyy, docket_type, case_num = x + assert ( + isinstance(yyyy, int) + and yyyy >= 2007 + and docket_type.upper() in {"-", "A", "M", "O"} + and isinstance(case_num, int) + ) + return str(yyyy)[2:4] + docket_type + str(case_num) + elif isinstance(x, str): + assert docket_number_regex.search(x) + return x[:3] + str(int(x[3:])) + + +def docket_priority(x: list) -> list: + """Sort key for docket numbers in order of 1) type ('-', 'A', 'M'), 2) year DESC, + 3) case number ASC. This prioritizes most recent dockets, then appeals, then motions. + """ + type_priority = {"-": 0, "A": 1, "M": 2, "O": 3} + term, mod, casenum = dedocket(x) + return (type_priority[mod], 1 / term, casenum) + + +"""Date utilities""" + + +def makedate(d, **kwargs) -> date: + """Allow `datetime` objects to bypass `dateparser.parse`; a TypeError would + otherwise be raised.""" + if isinstance(d, (datetime, date)) or d is None: + return d + else: + # return dateparser.parse(d) + return dateutil.parser.parse(d, **kwargs) + + +def next_weekday(d: date) -> date: + """Bump the input date object to the next weekday if it falls on a weekend.""" + if (reldate := d.weekday() - 7) < -2: + # it's a weekday; no-op + return d + else: + return d + timedelta(hours=(-24 * reldate)) + + +def first_monday_oct(year: int) -> date: + """Find the first Monday in October of `year`.""" + for i in range(1, 8): + if (term_start := date(year, 10, i)).weekday() == 0: + return term_start + + +def which_term(d: date) -> int: + """Returns the SCOTUS term year for the passed date.""" + _date = makedate(d) + if _date >= first_monday_oct(_date.year): + return _date.year + else: + return _date.year - 1 + + +def current_term() -> date: + """Returns the current SCOTUS term year, running Oct-Sep.""" + return which_term(date.today()) + + +def current_term_start(): + """Returns the current SCOTUS term start date.""" + return first_monday_oct(current_term().year) + + +def parse_filing_timestamp(ts): + """Instantiate a datetime.datetime object with the parsed result of + the 17-digit timestamp embedded in PDF filing URLs. + + From: + + + + take '20210609120636723'. + """ + argz = ( + ts[:4], + ts[4:6], + ts[6:8], + ts[8:10], + ts[10:12], + ts[12:14], + ts[14:] + "000", + ) + return datetime(*[int(x) for x in argz]) + + +"""Miscellaneous utilities""" + + +def make_hash(b: bytes) -> str: + """Make a unique ID. ETag and Last-Modified from courts cannot be + trusted. + """ + return sha1(b, usedforsecurity=False).hexdigest() + + +def chunker(iterable, chunksize=100): + """Break a large iterable into a generator of smaller chunks. + + Note + ==== + Iterating over `iterable` in a listcomp will return None when `chunksize` + is greater than `len(iterable)` because the `iterable` is always exhausted + before the listcomp can be completed. Use this instead.""" + + # wrap `iterable` if it doesn't quack like an iterable + if not hasattr(iterable, "__next__"): + iterable = iter(iterable) + + while True: + chunk = [] + for i in range(chunksize): + try: + chunk.append(next(iterable)) + except StopIteration: + # yield what has been iterated so far + break + if chunk == []: + return # not StopIteration; see PEP 479 para 10 + else: + yield chunk diff --git a/juriscraper/lib/exceptions.py b/juriscraper/lib/exceptions.py index 488d0c994..38e79953a 100644 --- a/juriscraper/lib/exceptions.py +++ b/juriscraper/lib/exceptions.py @@ -45,3 +45,32 @@ class PacerLoginException(Exception): def __init__(self, message): Exception.__init__(self, message) + + +class DocketNotFound(JuriscraperException): + """Use when a docket number request returns the file not found error page.""" + + def __init__(self, message): + super().__init__(self, message) + + +class DocketMissingJSON(JuriscraperException): + """Use when a docket number request returns a JSONDecodeError.""" + + def __init__(self, message): + super().__init__(self, message) + + +class AccessDeniedError(JuriscraperException): + """Raise when supremecourt.gov returns an 'Access Denied' page. + + + Access Denied + +

Access Denied

+ + You don't have permission to access "http://www.supremecourt.gov/docket/docket.aspx" on this server.

+ Reference #18.1c0f2417.1709421040.28130f66 + + + """ diff --git a/requirements.txt b/requirements.txt index e4f8477c1..f92c2b0b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ python-dateutil>=2.8.2 requests>=2.20.0 selenium>=4.9.1 tldextract +pymupdf diff --git a/tests/__init__.py b/tests/__init__.py index 245b22b47..86d3a934e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -10,7 +10,13 @@ TESTS_ROOT_EXAMPLES = os.path.join(TESTS_ROOT, "examples") TESTS_ROOT_EXAMPLES_PACER = os.path.join(TESTS_ROOT_EXAMPLES, "pacer") TESTS_ROOT_EXAMPLES_LASC = os.path.join(TESTS_ROOT_EXAMPLES, "lasc") - +TESTS_ROOT_EXAMPLES_SCOTUS = os.path.join( + TESTS_ROOT_EXAMPLES, + "dockets", + "united_states", + "federal_appellate", + "scotus", +) def test_local(): return unittest.TestLoader().discover("./tests/local") diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/SCOTUS_home_20240304.html b/tests/examples/dockets/united_states/federal_appellate/scotus/SCOTUS_home_20240304.html new file mode 100644 index 000000000..c9e712a32 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/SCOTUS_home_20240304.html @@ -0,0 +1,1078 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home - Supreme Court of the United States + + + + +

+
+ + + + +
+ + + + + + + + + + + + +
+ + + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+ +
+ + +
+ Search +
+
+
+
+
+
+ + +
+
+
+ + +
+ +
+ +
+
+ +
+ +
+
+
+
+ + + + +
+ +
+ + + + +
+
+
+
+
+ +
+ + +
+

Today at the Court - Monday, Mar 4, 2024


+
+
    +
  • The Supreme Court Building is open to the public from 9 a.m. to 3 p.m.
  • +
  • The Court will release an order list at 9:30 a.m.
  • +
  •  The Court may announce opinions on the homepage beginning at 10 a.m. The Court will not take the Bench.
  • +
  • +

    Courtroom Lectures available within the next 30 days.

    +
  • +
+
+
+ + +
+
+
+
+
+ + + + + + + + + + +
+ Calendar +
+ + + + + + + + + +
+ Title and navigation +
Title and navigation
<<<March 2024><<
+ + + + + + + + + + + + + + + + + + + +
+ March 2024 +
SMTWTFS
     12
3456789
1011121314
+
+  
+
16
17181920212223
24252627282930
31      
+
+ +
+ + +
+ Calendar Info/Key

+
+ + + + + + + + + + +
+ +
+
+

 

+ +
+ + + +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+ +

Recent Decisions

+

+
+ +
+ March 04, 2024 + + +
+   + +   +   +   +   +
+
+ Trump v. Anderson (23-719) + (Per Curiam) + +
+
+ + Because the Constitution makes Congress, rather than the States, responsible for enforcing Section 3 of the Fourteenth Amendment against federal officeholders and candidates, the Colorado Supreme Court erred in ordering former President Trump excluded from the 2024 Presidential primary ballot. +
+
+ +
+

+ +
+ February 21, 2024 + + +
+   + +   +   +   +   +
+
+ McElrath v. Georgia (22-721) + + +
+
+ + The jury’s verdict that the defendant was not guilty by reason of insanity of malice murder constituted an acquittal for double jeopardy purposes notwithstanding any inconsistency with the jury’s other verdicts. +
+
+ +
+   + +   +   +   +   +
+
+ Great Lakes Ins. SE v. Raiders Retreat Realty Co. (22-500) + + +
+
+ + Choice-of-law provisions in maritime contracts are presumptively enforceable under federal maritime law, with narrow exceptions not applicable in this case. +
+
+ +
+

+ +
+ February 08, 2024 + + +
+   + +   +   +   +   +
+
+ Department of Agriculture Rural Development Rural Housing Service v. Kirtz (22-846) + + +
+
+ + A consumer may sue a federal agency under 15 U. S. C. §§1681n and 1681o for defying the terms of the Fair Credit Reporting Act. +
+
+ +
+   + +   +   +   +   +
+
+ Murray v. UBS Securities, LLC (22-660) + + +
+
+ + A whistleblower seeking to invoke the protections of the Sarbanes-Oxley Act—18 U. S. C. §1514A(a)—must prove that their protected activity was a contributing factor in the employer’s unfavorable personnel action, but need not prove that the employer acted with “retaliatory intent.” +
+
+ +
+

+ + More Opinions... +
+
+
+ +
+ +
+ +
+
+
+
+

Did You Know...

+

A “New” Portrait for a 230th Birthday!

+
+

March 5th marks the 230th birthday of Justice Robert C. Grier, who served on the Court from 1846 to 1870. Born in Cumberland County, Pennsylvania, in 1794, Grier attended Dickinson College and practiced law privately until 1833, when he was appointed to the District Court of Allegheny County. In 1846, President James K. Polk appointed him to the Supreme Court. In 2023, descendants of Justice Grier donated this posthumous portrait, shared here for the first time.

+

 

+
+
+
+
+ Bust-length portrait of Associate Justice Robert C. Grier by Robert David Gauley, 1919. +
+
+ Bust-length portrait of Associate Justice Robert C. Grier
by Robert David Gauley, 1919. +
Collection of the Supreme Court of the United States +
+

+
+
+ +
+
+ +
+ + + + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_23-175.json b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_23-175.json new file mode 100644 index 000000000..87744d8d4 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_23-175.json @@ -0,0 +1 @@ +{"CaseNumber":"23-175 ","bCapitalCase":false,"sJsonCreationDate":"01/26/2024","sJsonTerm":"2023","sJsonCaseNumber":"00175","sJsonCaseType":"Paid","RelatedCaseNumber":[],"PetitionerTitle":"City of Grants Pass, Oregon, Petitioner","RespondentTitle":"Gloria Johnson, et al., on Behalf of Themselves and All Others Similarly Situated","DocketedDate":"August 25, 2023","LowerCourt":"United States Court of Appeals for the Ninth Circuit","LowerCourtCaseNumbers":"(20-35752, 20-35881)","LowerCourtDecision":"September 28, 2022","LowerCourtRehearingDenied":"July 5, 2023","QPLink":"../qp/23-00175qp.pdf","ProceedingsandOrder":[{"Date":"Aug 22 2023","Text":"Petition for a writ of certiorari filed. (Response due September 25, 2023)","Links":[{"Description":"Petition","File":"Grants Pass v. Johnson_cert petition_corrected.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/275911/20230823153037814_Grants%20Pass%20v.%20Johnson_cert%20petition_corrected.pdf"},{"Description":"Appendix","File":"Grants Pass Pet. App.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/275911/20230822105225753_Grants%20Pass%20Pet.%20App.pdf"},{"Description":"Certificate of Word Count","File":"Grants Pass Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/275911/20230822105236830_Grants%20Pass%20Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"Grants Pass Certificate of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/275911/20230822105246611_Grants%20Pass%20Certificate%20of%20Service.pdf"}]},{"Date":"Sep 11 2023","Text":"Brief amici curiae of Freddy Brown, et al. filed.","Links":[{"Description":"Main Document","File":"23-175acFreddyBrownEtAl.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279300/20230911133115951_23-175acFreddyBrownEtAl.pdf"},{"Description":"Proof of Service","File":"No. 23-175 CoS.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279300/20230911133206543_No.%2023-175%20CoS.pdf"},{"Description":"Certificate of Word Count","File":"No. 23-175 CoC.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279300/20230911133143784_No.%2023-175%20CoC.pdf"}]},{"Date":"Sep 19 2023","Text":"Brief amici curiae of California State Association of Counties filed.","Links":[{"Description":"Main Document","File":"CSAC_Cal Cities Amicus Brief in Support of Petition.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279928/20230919172655215_CSAC_Cal%20Cities%20Amicus%20Brief%20in%20Support%20of%20Petition.pdf"},{"Description":"Certificate of Word Count","File":"CSAC_Cal Cities Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279928/20230919172736245_CSAC_Cal%20Cities%20Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"CSAC_Cal Cities Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279928/20230919172758224_CSAC_Cal%20Cities%20Affidavit%20of%20Service.pdf"}]},{"Date":"Sep 19 2023","Text":"Brief amicus curiae of Goldwater Institute filed.","Links":[{"Description":"Main Document","File":"GP Amicus Brief.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279897/20230919154157625_GP%20Amicus%20Brief.pdf"},{"Description":"Certificate of Word Count","File":"GP Cert Compl.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279897/20230919154213479_GP%20Cert%20Compl.pdf"},{"Description":"Proof of Service","File":"GP Proof of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279897/20230919154224278_GP%20Proof%20of%20Service.pdf"}]},{"Date":"Sep 20 2023","Text":"Waiver of right of respondent Gloria Johnson, et al. to respond filed.","Links":[{"Description":"Main Document","File":"No 23-175 Grants Pass v. Johnson Response Waiver.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279974/20230920123132620_No%2023-175%20Grants%20Pass%20v.%20Johnson%20Response%20Waiver.pdf"}]},{"Date":"Sep 20 2023","Text":"Brief amicus curiae of District Attorney of Sacramento County filed.","Links":[{"Description":"Main Document","File":"23-175 Locher brief.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280081/20230920180102844_23-175%20Locher%20brief.pdf"},{"Description":"Certificate of Word Count","File":"23-175 Locher Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280081/20230920180123016_23-175%20Locher%20Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"23-175 Locher Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280081/20230920180150915_23-175%20Locher%20Affidavit%20of%20Service.pdf"}]},{"Date":"Sep 20 2023","Text":"Brief amici curiae of Speaker of the Arizona House of Representatives Ben Toma, et al. filed.","Links":[{"Description":"Main Document","File":"23-175 City of Grants Pass v Johnson - AZ Leg amicus Final.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279986/20230920131054653_23-175%20City%20of%20Grants%20Pass%20v%20Johnson%20-%20AZ%20Leg%20amicus%20Final.pdf"},{"Description":"Certificate of Word Count","File":"Certificate Word Count FINAL.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279986/20230920131105798_Certificate%20Word%20Count%20FINAL.pdf"},{"Description":"Proof of Service","File":"Proof of Service FINAL.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/279986/20230920131123052_Proof%20of%20Service%20FINAL.pdf"}]},{"Date":"Sep 20 2023","Text":"Brief amici curiae of California State Sheriffs' Association, et al. filed.","Links":[{"Description":"Main Document","File":"44262 Brief - Amici Curiae.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280078/20230920180047655_44262%20Brief%20-%20Amici%20Curiae.pdf"},{"Description":"Certificate of Word Count","File":"44262 Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280078/20230920180109978_44262%20Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"44262 Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280078/20230920180126573_44262%20Affidavit%20of%20Service.pdf"}]},{"Date":"Sep 21 2023","Text":"Brief amicus curiae of Pacific Legal Foundation and California Business Properties Association filed.","Links":[{"Description":"Main Document","File":"Grants Pass AC Brief.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280139/20230921132350323_Grants%20Pass%20AC%20Brief.pdf"},{"Description":"Certificate of Word Count","File":"Grants Pass AC Cert of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280139/20230921132407025_Grants%20Pass%20AC%20Cert%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"PLF Amicus Proof of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280139/20230921132417264_PLF%20Amicus%20Proof%20of%20Service.pdf"}]},{"Date":"Sep 21 2023","Text":"Brief amicus curiae of Office of San Diego County District Attorney filed.","Links":[{"Description":"Main Document","File":"23-175 Amicus Brief San Diego County District Attorney.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280167/20230921154754114_23-175%20Amicus%20Brief%20San%20Diego%20County%20District%20Attorney.pdf"},{"Description":"Certificate of Word Count","File":"23-175 Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280167/20230921154811699_23-175%20Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"23-175 Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280167/20230921154839405_23-175%20Affidavit%20of%20Service.pdf"}]},{"Date":"Sep 22 2023","Text":"Brief amici curiae of League of Oregon Cities, et al. filed.","Links":[{"Description":"Main Document","File":"Oregon Cties MAIN E FILE Sept 22 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280303/20230922211040796_Oregon%20Cties%20MAIN%20E%20FILE%20Sept%2022%2023.pdf"},{"Description":"Proof of Service","File":"Oregon Cties Amicus Cert Svc E FILE Sept 22 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280303/20230922211057830_Oregon%20Cties%20Amicus%20Cert%20Svc%20E%20FILE%20Sept%2022%2023.pdf"},{"Description":"Certificate of Word Count","File":"Oregon Cties Amicus Cert WC E FILE Sept 22 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280303/20230922211049868_Oregon%20Cties%20Amicus%20Cert%20WC%20E%20FILE%20Sept%2022%2023.pdf"}]},{"Date":"Sep 22 2023","Text":"Brief amicus curiae of California Governor Gavin Newsom filed.","Links":[{"Description":"Main Document","File":"Amicus Brief for Governor Newsom - Grants Pass_Final.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280288/20230922163648635_Amicus%20Brief%20for%20Governor%20Newsom%20-%20Grants%20Pass_Final.pdf"},{"Description":"Certificate of Word Count","File":"Cert_of_Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280288/20230922163701408_Cert_of_Compliance.pdf"},{"Description":"Proof of Service","File":"Cert_of_Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280288/20230922163711926_Cert_of_Service.pdf"}]},{"Date":"Sep 22 2023","Text":"Brief amicus curiae of Brentwood Community Council filed.","Links":[{"Description":"Main Document","File":"44268 pdf Jordan br.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280292/20230922165706540_44268%20pdf%20Jordan%20br.pdf"},{"Description":"Proof of Service","File":"44268 Jordan Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280292/20230922165740559_44268%20Jordan%20Affidavit%20of%20Service.pdf"},{"Description":"Certificate of Word Count","File":"44268 Jordan Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280292/20230922165727704_44268%20Jordan%20Certificate%20of%20Compliance.pdf"},{"Description":"Other","File":"44268 pdf Jordan app.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280292/20230922165717202_44268%20pdf%20Jordan%20app.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of City and County of San Francisco, et al. filed","Links":[{"Description":"Main Document","File":"Brief.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280445/20230925181510233_Brief.pdf"},{"Description":"Certificate of Word Count","File":"Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280445/20230925181525338_Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280445/20230925181632302_Affidavit%20of%20Service.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of LA Alliance for Human Rights, et al. filed.","Links":[{"Description":"Main Document","File":"0081_001.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280474/20230928150837413_0081_001.pdf"},{"Description":"Proof of Service","File":"0082_001.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280474/20230928150849945_0082_001.pdf"},{"Description":"Certificate of Word Count","File":"0083_001.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280474/20230928150900772_0083_001.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of Idaho, et al. filed.","Links":[{"Description":"Main Document","File":"No 23-175_AmicusBrief.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280428/20230925170042238_No%2023-175_AmicusBrief.pdf"},{"Description":"Certificate of Word Count","File":"No 23-175_Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280428/20230925170058062_No%2023-175_Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"No 23-175_Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280428/20230925170111116_No%2023-175_Affidavit%20of%20Service.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of Los Angeles Area Chamber of Commerce, et al. filed.","Links":[{"Description":"Main Document","File":"2023-09-25 ACB.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280422/20230925164548642_2023-09-25%20ACB.pdf"},{"Description":"Proof of Service","File":"2023-09-25 Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280422/20230925164607188_2023-09-25%20Affidavit%20of%20Service.pdf"},{"Description":"Certificate of Word Count","File":"2023-09-25 Cert.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280422/20230925164558580_2023-09-25%20Cert.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amicus curiae of Venice Stakeholders Association filed.","Links":[{"Description":"Main Document","File":"Venice Amicus MAIN E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280440/20230925174848059_Venice%20Amicus%20MAIN%20E%20FILE%20Sep%2025%2023.pdf"},{"Description":"Certificate of Word Count","File":"Venice Amicus Cert WC E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280440/20230925174855687_Venice%20Amicus%20Cert%20WC%20E%20FILE%20Sep%2025%2023.pdf"},{"Description":"Proof of Service","File":"Venice Amicus Cert Svc E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280440/20230925174910552_Venice%20Amicus%20Cert%20Svc%20E%20FILE%20Sep%2025%2023.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of City of Phoenix, et al. filed.","Links":[{"Description":"Main Document","File":"City of Phoenix Amicus Brief 9-22-23 FINAL Version.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280385/20230925144546598_City%20of%20Phoenix%20Amicus%20Brief%209-22-23%20FINAL%20Version.pdf"},{"Description":"Proof of Service","File":"Proof of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280385/20230925144559773_Proof%20of%20Service.pdf"},{"Description":"Certificate of Word Count","File":"Certificate of Word Count.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280385/20230925144553582_Certificate%20of%20Word%20Count.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amicus curiae of City of Los Angeles filed.","Links":[{"Description":"Main Document","File":"23-175 Amicus City of LA - Final.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280395/20230925153022862_23-175%20Amicus%20City%20of%20LA%20-%20Final.pdf"},{"Description":"Proof of Service","File":"Proof of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280395/20230925153037996_Proof%20of%20Service.pdf"},{"Description":"Certificate of Word Count","File":"Certificate Word Count.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280395/20230925153031983_Certificate%20Word%20Count.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amicus curiae of Washington State Association of Sheriffs and Police Chiefs filed.","Links":[{"Description":"Main Document","File":"Waspc Amicus MAIN E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280438/20230925174205734_Waspc%20Amicus%20MAIN%20%20E%20FILE%20Sep%2025%2023.pdf"},{"Description":"Proof of Service","File":"Waspc Amicus Cert Svc E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280438/20230925174220937_Waspc%20Amicus%20Cert%20Svc%20E%20FILE%20Sep%2025%2023.pdf"},{"Description":"Certificate of Word Count","File":"Waspc Amicus Cert WC E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280438/20230925174213844_Waspc%20Amicus%20Cert%20WC%20E%20FILE%20Sep%2025%2023.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of Ten California Cities and The County of Orange filed.","Links":[{"Description":"Main Document","File":"SoCalMuni MAIN E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280436/20230925173704449_SoCalMuni%20MAIN%20E%20FILE%20Sep%2025%2023.pdf"},{"Description":"Proof of Service","File":"SoCalMuni Cert Svc E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280436/20230925173722814_SoCalMuni%20Cert%20Svc%20E%20FILE%20Sep%2025%2023.pdf"},{"Description":"Certificate of Word Count","File":"SoCalMuni Cert WC E FILE Sep 25 23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280436/20230925173712520_SoCalMuni%20Cert%20WC%20E%20FILE%20Sep%2025%2023.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of Bay Area Council, et al. filed.","Links":[{"Description":"Main Document","File":"23-175 Amici Brief BAC et al - PDFA.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280372/20230925134906047_23-175%20Amici%20Brief%20BAC%20et%20al%20-%20PDFA.pdf"},{"Description":"Proof of Service","File":"23-175 Amicis BAC et al. COS- PDFA.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280372/20230925134921349_23-175%20Amicis%20BAC%20et%20al.%20COS-%20PDFA.pdf"},{"Description":"Certificate of Word Count","File":"23-175 Amicis BAC et al. COC- PDFA.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280372/20230925134914947_23-175%20Amicis%20BAC%20et%20al.%20COC-%20PDFA.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of Neighbors for a Better San Francisco, et al. filed.","Links":[{"Description":"Main Document","File":"Grants Pass Brief to Print.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280388/20230925145056894_Grants%20Pass%20Brief%20to%20Print.pdf"},{"Description":"Certificate of Word Count","File":"23-175 Quinn Emanuel CoC.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280388/20230925145638431_23-175%20Quinn%20Emanuel%20CoC.pdf"},{"Description":"Proof of Service","File":"23-175 Quinn Emanuel CoS.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280388/20230925145648002_23-175%20Quinn%20Emanuel%20CoS.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amici curiae of International Municipal Lawyers Association, et al. filed.","Links":[{"Description":"Main Document","File":"IMLA Amicus Brief FINAL DRAFT.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280374/20230925135328591_IMLA%20Amicus%20Brief%20FINAL%20DRAFT.pdf"},{"Description":"Proof of Service","File":"Proof of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280374/20230925135350977_Proof%20of%20Service.pdf"},{"Description":"Certificate of Word Count","File":"Certificate of Word Count.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280374/20230925135343642_Certificate%20of%20Word%20Count.pdf"}]},{"Date":"Sep 25 2023","Text":"Brief amicus curiae of Criminal Justice Legal Foundation filed.","Links":[{"Description":"Main Document","File":"GrantsPassCJLF_AmicusCert.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280443/20230925175212019_GrantsPassCJLF_AmicusCert.pdf"},{"Description":"Proof of Service","File":"GrantsPassCJLF_Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280443/20230925175225087_GrantsPassCJLF_Service.pdf"},{"Description":"Certificate of Word Count","File":"GrantsPassCJLF_WordCount.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/280443/20230925175240563_GrantsPassCJLF_WordCount.pdf"}]},{"Date":"Oct 04 2023","Text":"DISTRIBUTED for Conference of 10/27/2023."},{"Date":"Oct 05 2023","Text":"Response Requested. (Due November 6, 2023)"},{"Date":"Oct 10 2023","Text":"Motion to extend the time to file a response from November 6, 2023 to December 6, 2023, submitted to The Clerk.","Links":[{"Description":"Main Document","File":"No 23-175 Grants Pass BIO extension letter 10.10.23.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/284631/20231010200941976_No%2023-175%20Grants%20Pass%20BIO%20extension%20letter%2010.10.23.pdf"}]},{"Date":"Oct 12 2023","Text":"Motion to extend the time to file a response is granted and the time is extended to and including December 6, 2023."},{"Date":"Nov 03 2023","Text":"Brief amicus curiae of City of Chico filed.","Links":[{"Description":"Main Document","File":"Amicus Brief City of Chico.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/288850/20231103180934467_Amicus%20Brief%20City%20of%20Chico.pdf"},{"Description":"Proof of Service","File":"Affidavit of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/288850/20231103180958627_Affidavit%20of%20Service.pdf"},{"Description":"Certificate of Word Count","File":"Certificate of Word Count City of Chico.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/288850/20231103180944332_Certificate%20of%20Word%20Count%20City%20of%20Chico.pdf"}]},{"Date":"Dec 06 2023","Text":"Brief of respondents Gloria Johnson, et al. in opposition filed.","Links":[{"Description":"Main Document","File":"No 23-175 Grants Pass v Johnson BIO Final.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/292381/20231206101030999_No%2023-175%20Grants%20Pass%20v%20Johnson%20BIO%20Final.pdf"},{"Description":"Certificate of Word Count","File":"Certificate Word Count.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/292381/20231206101116287_Certificate%20Word%20Count.pdf"},{"Description":"Proof of Service","File":"Proof of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/292381/20231206101132940_Proof%20of%20Service.pdf"}]},{"Date":"Dec 20 2023","Text":"DISTRIBUTED for Conference of 1/5/2024."},{"Date":"Dec 20 2023","Text":"Reply of petitioner City of Grants Pass filed. (Distributed)","Links":[{"Description":"Main Document","File":"Grants Pass v. Johnson_cert reply_final.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/293791/20231220101949989_Grants%20Pass%20v.%20Johnson_cert%20reply_final.pdf"},{"Description":"Certificate of Word Count","File":"Grants Pass - Cert Reply Certificate of Compliance.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/293791/20231220102011573_Grants%20Pass%20-%20Cert%20Reply%20Certificate%20of%20Compliance.pdf"},{"Description":"Proof of Service","File":"Grants Pass - Cert Reply Certificate of Service.pdf","DocumentUrl":"http://www.supremecourt.gov/DocketPDF/23/23-175/293791/20231220102035311_Grants%20Pass%20-%20Cert%20Reply%20Certificate%20of%20Service.pdf"}]},{"Date":"Jan 08 2024","Text":"DISTRIBUTED for Conference of 1/12/2024."},{"Date":"Jan 12 2024","Text":"Petition GRANTED."}],"AttorneyHeaderPetitioner":"Attorneys for Petitioner","Petitioner":[{"Attorney":"Theane D. Evangelis","IsCounselofRecord":true,"Title":"Gibson, Dunn & Crutcher LLP","PrisonerId":null,"Phone":"(213) 229-7000","Address":"333 South Grand Avenue","City":"Los Angeles","State":"CA","Zip":"90071","Email":"TEVANGELIS@GIBSONDUNN.COM","PartyName":"City of Grants Pass"}],"AttorneyHeaderRespondent":"Attorneys for Respondents","Respondent":[{"Attorney":"Kelsi Brown Corkran","IsCounselofRecord":true,"Title":"Institute for Constitutional Advocacy & Protection","PrisonerId":null,"Phone":"202-661-6728","Address":"600 New Jersey Ave NW","City":"Washington","State":"DC","Zip":"20001","Email":"KBC74@GEORGETOWN.EDU","PartyName":"Gloria Johnson, et al."},{"Attorney":"Edward Beman Johnson","IsCounselofRecord":false,"Title":"Oregon Law Center","PrisonerId":null,"Phone":"503-998-2133","Address":"522 SW Fifth Avenue. Suite 812","City":"Portland","State":"OR","Zip":"97204","Email":"ejohnson@oregonlawcenter.org","PartyName":"Gloria Johnson, et al."}],"AttorneyHeaderOther":"Other","Other":[{"Attorney":"James E. Preston","IsCounselofRecord":true,"Title":"Hauf & Forsythe","PrisonerId":null,"Phone":"202-643-3917","Address":"712 H. Street NE\r\nSuite 739","City":"Washington","State":"DC","Zip":"20002","Email":"jep@prestonlawintnl.com","PartyName":"National Coalition for Men"},{"Attorney":"Eric Samuel Boorstin","IsCounselofRecord":true,"Title":"Horvitz & Levy, LLP","PrisonerId":null,"Phone":"8189950800","Address":"3601 W. Olive Avenue, 8th Floor","City":"Burbank","State":"CA","Zip":"91505","Email":"eboorstin@horvitzlevy.com","PartyName":"Los Angeles Area Chamber of Commerce and Central City Association of Los Angeles"},{"Attorney":"David Ryan Carpenter","IsCounselofRecord":true,"Title":"Sidley Austin LLP","PrisonerId":null,"Phone":"213-896-6000","Address":"555 West Fifth Street, Suite 4000","City":"Los Angeles","State":"CA","Zip":"90013","Email":"drcarpenter@sidley.com","PartyName":"California Governor Gavin Newsom"},{"Attorney":"David Carrillo Casarrubias","IsCounselofRecord":true,"Title":"Hanson Bridgett LLP","PrisonerId":null,"Phone":"415-995-5893","Address":"425 Market Street, 26th Floor","City":"San Francisco","State":"CA","Zip":"94105","Email":"dcasarrubias@hansonbridgett.com","PartyName":"Bay Area Council et al."},{"Attorney":"Fred D. Heather","IsCounselofRecord":true,"Title":"Glaser Weil Fink Jordan Avchen & Shapiro LLP","PrisonerId":null,"Phone":"310.553.3000","Address":"10250 Constellation Blvd.\r\n19th Floor","City":"Los Angeles","State":"CA","Zip":"90067","Email":"fheather@glaserweil.com","PartyName":"Brentwood Community Council"},{"Attorney":"Jennifer Bacon Henning","IsCounselofRecord":true,"Title":"County Counsels' Association of California","PrisonerId":null,"Phone":"916-327-7535","Address":"1100 K Street, Suite 101","City":"Sacramento","State":"CA","Zip":"95814","Email":"jhenning@counties.org","PartyName":"California State Association of Counties"},{"Attorney":"Tiffany Joy Israel","IsCounselofRecord":true,"Title":"Aleshire & Wynder LLP","PrisonerId":null,"Phone":"(949) 223-1170","Address":"1 Park Plaza. Suite 1000","City":"Irvine","State":"CA","Zip":"92614","Email":"tisrael@awattorneys.com","PartyName":"Ten California Cities and The County of Orange"},{"Attorney":"Ronald Alan Jakob","IsCounselofRecord":true,"Title":"Office of the District Attorney","PrisonerId":null,"Phone":"619-531-3671","Address":"330 West Broadway\r\nSuite 860","City":"San Diego","State":"CA","Zip":"92101-3827","Email":"Ronald.Jakob@sdcda.org","PartyName":"District Attorney of San Diego County"},{"Attorney":"Anit Kumar Jindal","IsCounselofRecord":true,"Title":"Markowitz Herbold PC","PrisonerId":null,"Phone":"(503) 295-3085","Address":"1455 SW Broadway, Suite 1900","City":"Portland","State":"OR","Zip":"97201","Email":"anitjindal@markowitzherbold.com","PartyName":"League of Oregon Cities et al."},{"Attorney":"Jeffrey Lewis","IsCounselofRecord":true,"Title":"Jeff Lewis Law, APC","PrisonerId":null,"Phone":"(310) 935-4001","Address":"827 Deep Valley Drive\r\nSuite 209","City":"Rolling Hills Estates","State":"CA","Zip":"90274","Email":"Jeff@JeffLewisLaw.com","PartyName":"Venice Stakeholders Association"},{"Attorney":"Albert Calvin Locher","IsCounselofRecord":true,"Title":"Sacramento County Dist.Attorney's Office","PrisonerId":null,"Phone":"916-505-2492","Address":"901 G Street","City":"Sacramento","State":"CA","Zip":"95814","Email":"lochera@sacda.org","PartyName":"District Attorney of Sacramento County"},{"Attorney":"Robert E. Mack","IsCounselofRecord":true,"Title":"Smith Alling, P.S.","PrisonerId":null,"Phone":"(253) 627-1091","Address":"1501 Dock Street","City":"Tacoma","State":"WA","Zip":"98402","Email":"rmack@smithalling.com","PartyName":"Washington State Association of Sheriffs and Police Chiefs"},{"Attorney":"Christopher George Michel","IsCounselofRecord":true,"Title":"Quinn Emanuel Urquhart & Sullivan, LLP","PrisonerId":null,"Phone":"2025388308","Address":"1300 I St. NW\r\nSuite 900","City":"Washington","State":"DC","Zip":"20005","Email":"CHRISTOPHERMICHEL@QUINNEMANUEL.COM","PartyName":"Neighbors for a Better San Francisco et al."},{"Attorney":"Mark Miller","IsCounselofRecord":true,"Title":"Pacific Legal Foundation","PrisonerId":null,"Phone":"916-503-9001","Address":"4440 PGA Blvd.\r\nSuite 307","City":"Palm Beach Gardens","State":"FL","Zip":"33410","Email":"mark@pacificlegal.org","PartyName":"Pacific Legal Foundation and California Business Properties Association"},{"Attorney":"Justin Scott Pierce","IsCounselofRecord":true,"Title":"Pierce Coleman PLLC","PrisonerId":null,"Phone":"(602) 772-5506","Address":"7730 E. Greenway Road, Suite 105","City":"Scottsdale","State":"AZ","Zip":"85260","Email":"Justin@piercecoleman.com","PartyName":"City of Phoenix & the League of Arizona Cities and Towns"},{"Attorney":"Brandon Michael Rain","IsCounselofRecord":true,"Title":"Seattle City Attorney's Office","PrisonerId":null,"Phone":"206-684-8200","Address":"701 Fifth Avenue, Suite 2050","City":"Seattle","State":"WA","Zip":"98104","Email":"brandon.rain@seattle.gov","PartyName":"International Municipal Lawyers Association, National League of Cities, National Association of Counties, North Dakota League of Cities, Cities of Albuquerque, Anchorage, Colorado Springs, Henderson, Las Vegas, Milwaukee, Providence, Redondo Beach, Saint Paul, San Diego, Seattle, Spokane, and Tacoma, the City and County of Honolulu, and the County of San Bernardino"},{"Attorney":"Denise Lynn Rocawich","IsCounselofRecord":false,"Title":"Jones & Mayer","PrisonerId":null,"Phone":"714446-1400","Address":"3777 N. Harbor Blvd.","City":"Fullerton","State":"CA","Zip":"92835","Email":"dlr@jones-mayer.com","PartyName":"California State Sheriffs' Association, et al."},{"Attorney":"Brunn Wall Roysden III","IsCounselofRecord":true,"Title":"Fusion Law, PLLC","PrisonerId":null,"Phone":"6023157545","Address":"7600 N. 15th St., Suite 150","City":"Phoenix","State":"AZ","Zip":"85020","Email":"beau@fusion.law","PartyName":"Speaker of the Arizona House of Representatives Ben Toma and President of the Arizona State Senate Warren Petersen"},{"Attorney":"Eric Golas Salbert","IsCounselofRecord":true,"Title":"Alvarez-Glasman & Colvin","PrisonerId":null,"Phone":"562-699-5500","Address":"13181 Crossroads Parkway North - West Tower, Suite 400","City":"City of Industry","State":"CA","Zip":"91746","Email":"esalbert@agclawfirm.com","PartyName":"City of Chico"},{"Attorney":"Timothy Mason Sandefur","IsCounselofRecord":true,"Title":"Goldwater Institute","PrisonerId":null,"Phone":"6024625000","Address":"500 E. Coronado Road","City":"Phoenix","State":"AZ","Zip":"85004","Email":"TSANDEFUR@GOLDWATERINSTITUTE.ORG","PartyName":"Goldwater Institute"},{"Attorney":"Kent S. Scheidegger","IsCounselofRecord":true,"Title":"Criminal Justice Legal Fdtn.","PrisonerId":null,"Phone":"9164460345","Address":"2131 L Street","City":"Sacramento","State":"CA","Zip":"95816","Email":"briefs@cjlf.org","PartyName":"Criminal Justice Legal Foundation"},{"Attorney":"Tara Michelle Steeley","IsCounselofRecord":true,"Title":"Office of the City Attorney","PrisonerId":null,"Phone":"4155544655","Address":"City Hall, Room 234, 1 Dr. Carlton B. Goodlett","City":"San Francisco","State":"CA","Zip":"94102","Email":"Tara.steeley@sfcityatty.org","PartyName":"City and County of San Francisco and Mayor London Breed"},{"Attorney":"James R. Touchstone","IsCounselofRecord":true,"Title":"Jones & Mayer","PrisonerId":null,"Phone":"714-446-1400","Address":"3777 North Harbor Boulevard","City":"Fullerton","State":"CA","Zip":"92835","Email":"jrt@jones-mayer.com","PartyName":"California State Sheriffs' Association, et al."},{"Attorney":"Joshua Nathaniel Turner","IsCounselofRecord":true,"Title":"ldaho Attorney General's Office","PrisonerId":null,"Phone":"2083323548","Address":"700 W Jefferson Street","City":"Boise","State":"ID","Zip":"83720","Email":"josh.turner@ag.idaho.gov","PartyName":"States of Idaho, Montana, et. al."},{"Attorney":"Matthew Donald Umhofer","IsCounselofRecord":true,"Title":"Umhofer, Mitchell & King LLP","PrisonerId":null,"Phone":"213-394-7979","Address":"767 S. Alameda St.\r\nSuite 270","City":"Los Angeles","State":"CA","Zip":"90021","Email":"matthew@umklaw.com","PartyName":"LA Alliance for Human Rights"},{"Attorney":"Michael Martin Walsh","IsCounselofRecord":true,"Title":"City of Los Angeles, City Attorney","PrisonerId":null,"Phone":"(213) 978-2209","Address":"200 North Spring Street, 14th Floor","City":"Los Angeles","State":"CA","Zip":"90012","Email":"michael.walsh@lacity.org","PartyName":"City of Los Angeles"},{"Attorney":"Ilan Wurman","IsCounselofRecord":true,"Title":"Tully Bailey LLP","PrisonerId":null,"Phone":"4809652245","Address":"11811 N. Tatum Blvd. Suite 3031","City":"Phoenix","State":"AZ","Zip":"85028","Email":"iwurman@tullybailey.com","PartyName":"Freddy Brown, et al."}]} \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_JSON_not_found.html b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_JSON_not_found.html new file mode 100644 index 000000000..0b581fe72 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_JSON_not_found.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+
+ +
+ +
+ Search +
+
+
+
+
+
+ + +
+
+
+
+ Skip Navigation Links +
+
+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + +
+
+
+ +
+ +
+

ERROR: File or directory not found.

+
+ Most likely causes:
+
    +
  • There might be a typing error in the address.
  • +
  • If you clicked on a link, it may be out of date.
  • +
+ What you can try:
+ + +
+

 

+ + +
+ + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_access_denied.html b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_access_denied.html new file mode 100644 index 000000000..e2ed7a883 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_access_denied.html @@ -0,0 +1,13 @@ + + + + + +Access Denied + +

Access Denied

+ +You don't have permission to access "http://www.supremecourt.gov/docket/docket.aspx" on this server.

+Reference #18.1c0f2417.1709421040.28130f66 + + diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx new file mode 100644 index 000000000..fb4b77d45 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docket Search - Supreme Court of the United States + + + + +

+
+ + + + +
+ + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+
+ +
+ +
+ Search +
+
+
+
+
+
+ + +
+
+
+
+ Skip Navigation LinksHome > Case Documents > Docket Search +
+
+
+
+
+
+ + + +
+ + +
+

Docket Search

+
+
+

The Supreme Court’s docket system contains information about cases, both pending and decided, that have been filed at the Court. The docket provided + here contains complete information regarding the status of cases filed since the beginning of the 2001 Term.

+ +

Users can search for the docket in a particular case by using a Supreme Court docket number, a case name, or other words or numbers included on a docket + report. The format for Supreme Court docket numbers is "Term year-number" (e.g., 21-471; 22-5301).

+ +

Users can also sign up to receive email notifications of activity in pending cases. To do so, visit the docket page for an individual case and click on + the envelope icon that is just above the case number. You will be asked to enter an email address. When you click “Subscribe,” an email will be sent to + you with a link for you to confirm the correct email address. Once you click that link, you will receive email notifications every time there is a new + filing or action by the Court in the case. +

+ +

Questions Presented. The Questions Presented in a granted or noted case can be obtained by first obtaining the docket report for that case, then + clicking on the blue “Questions Presented” hyperlink located on the left side of the docket report. Once the hyperlink is clicked, a .pdf file setting + forth the Questions Presented in the case will appear. +

+ +
+ +   + + + + + + +
+ +
+ +
+
+
+ +    + +    + +    + +
+
+ +
+ +
+

+ + Note: The Engrossed Dockets from 1791 to 1995 have been scanned by the National Archives from its microfilm collection and are available in its Catalog. + + + + +


+ +
+

 

+ + +
+ + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page1.aspx b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page1.aspx new file mode 100644 index 000000000..015439427 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page1.aspx @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docket Search - Supreme Court of the United States + + + + +
+
+ + + + +
+ + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+
+ +
+ +
+ Search +
+
+
+
+
+
+ + +
+
+
+
+ Skip Navigation LinksHome > Case Documents > Docket Search +
+
+
+
+
+
+ + + +
+ + +
+

Docket Search

+
+
+

The Supreme Court’s docket system contains information about cases, both pending and decided, that have been filed at the Court. The docket provided + here contains complete information regarding the status of cases filed since the beginning of the 2001 Term.

+ +

Users can search for the docket in a particular case by using a Supreme Court docket number, a case name, or other words or numbers included on a docket + report. The format for Supreme Court docket numbers is "Term year-number" (e.g., 21-471; 22-5301).

+ +

Users can also sign up to receive email notifications of activity in pending cases. To do so, visit the docket page for an individual case and click on + the envelope icon that is just above the case number. You will be asked to enter an email address. When you click “Subscribe,” an email will be sent to + you with a link for you to confirm the correct email address. Once you click that link, you will receive email notifications every time there is a new + filing or action by the Court in the case. +

+ +

Questions Presented. The Questions Presented in a granted or noted case can be obtained by first obtaining the docket report for that case, then + clicking on the blue “Questions Presented” hyperlink located on the left side of the docket report. Once the hyperlink is clicked, a .pdf file setting + forth the Questions Presented in the case will appear. +

+ +
+ +   + + + + + + +
+ +
+ +
+
+ 499 items found. Page: 1 of 100 for your search: "feb 20, 2024"
+ << First +    + < Previous +    + Next > +    + Last >> +
+
+ +
+ Search Results: +
+ +
+ file type icon + +  Docket for 23A769
Title:Christine H. Scott, Applicant v. Florida
District Court of Appeal of Florida, Fourth District for a writ of certiorari from February 20, 2024 to April 20, 2024, submitted to Justice Thomas. Main DocumentLower Court Orders/Opinions
+ +
+ file type icon + +  Docket for 23-779
Title:David Forsythe, Petitioner v. Denis R. McDonough, Secretary of Veterans Affairs
Petition for a writ of certiorari filed. (Response due February 20, 2024 extend the time to file a response from February 20, 2024 to March 21, 2024, submitted to The Clerk.
+ +
+ file type icon + +  Docket for 23-726
Title:Mike Moyle, Speaker of the Idaho House of Representatives, et al., Petitioners v. United States
Mike Moyle, Speaker of the Idaho House of Representatives, et al., Petitioners are to be filed on or before Tuesday, February 20, 2024. Respondent's brief on the merits is to be Feb 20 2024
+ +
+ file type icon + +  Docket for 23-367
Title:Starbucks Corporation, Petitioner v. M. Kathleen McKinney, Regional Director of Region 15 of the National Labor Relations Board, for and on Behalf of the National Labor Relations Board
M. Kathleen McKinney, Regional Director of Region 15 of the National Labor Relations Board, for and on Behalf of the National Labor Relations Board Petition for a writ of certiorari filed Feb 20 2024
+ +
+ file type icon + +  Docket for 23-156
Title:Speech First, Inc., Petitioner v. Timothy Sands, Individually and in His Official Capacity as President of the University of Virginia Polytechnic Institute and State University
Timothy Sands, Individually and in His Official Capacity as President of the University of Virginia Polytechnic Institute and State University United States Court of Appeals for the Fourth Feb 20 2024
+ + +
+ +
+

+ + Note: The Engrossed Dockets from 1791 to 1995 have been scanned by the National Archives from its microfilm collection and are available in its Catalog. + + + + +


+ +
+

 

+ + +
+ + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page2.aspx b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page2.aspx new file mode 100644 index 000000000..2b905d046 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_results_page2.aspx @@ -0,0 +1,48 @@ +1|#||4|7086|updatePanel|ctl00_ctl00_MainEditable_mainContent_UpdatePanel1| +
+ +
+
+ 499 items found. Page: 2 of 100 for your search: "feb 20, 2024"
+ << First +    + < Previous +    + Next > +    + Last >> +
+
+ +
+ Search Results: +
+ +
+ file type icon + +  Docket for 23-463
Title:Elizabeth Brokamp, Petitioner v. Letitia James, Attorney General of New York, et al.
Letitia James, Attorney General of New York, et al. United States Court of Appeals for the the time to file a response is granted and the time is extended to and including February 20, 2024 Feb 20 2024
+ +
+ file type icon + +  Docket for 23-250
Title:Xavier Becerra, Secretary of Health and Human Services, et al., Petitioners v. San Carlos Apache Tribe
Xavier Becerra, Secretary of Health and Human Services, et al., Petitioners United States Court of Appeals for the Ninth Circuit Application (23A103) to extend the time to file a petition Feb 20 2024
+ +
+ file type icon + +  Docket for 22O141
Title:Texas, Plaintiff v. New Mexico and Colorado
Documents filed with the Special Master may be found at http://www.ca8.uscourts.gov/texas-v-new-mexico-and-colorado-no-141-original Motion for leave to file a bill of complaint filed. (Response due
+ +
+ file type icon + +  Docket for 23A481
Title:Pleasant View Baptist Church, et al., Applicants v. Andrew Beshear
for a writ of certiorari from January 1, 2024 to February 20, 2024, submitted to Justice Kavanaugh. Application (23A481) granted by Justice Kavanaugh extending the time to file until February 20, 2024
+ +
+ file type icon + +  Docket for 23-6803
Title:Eduardo Garcia Briseno, aka Eduardo Garcia Rodriguez, Petitioner v. United States
Eduardo Garcia Briseno, aka Eduardo Garcia Rodriguez, Petitioner United States Court of Appeals for the Fifth Circuit Feb 20 2024 Petition for a writ of certiorari and motion for leave to proceed in
+ + +
+ |0|hiddenField|__EVENTTARGET||0|hiddenField|__EVENTARGUMENT||192|hiddenField|__VIEWSTATE|2wVu6/Kqx9wW3YrRJ0VFCNa/CLdTCjCulgg+7QHVXUqDx4a0CQiBKAbnHENR7u6/tyqtGniywGCL3ftJYJnR9bu8a73LSp4wjr22Cfchq6WsXiLHJPh2IFcg0LW31HTlegKMIJKItcUW4ypilR4VdwDKrdTHBQ8bjbcBzSJR8vyTf3bFa79uCeYtOh7LJyky|8|hiddenField|__VIEWSTATEGENERATOR|71826567|0|asyncPostBackControlIDs|||0|postBackControlIDs|||51|updatePanelIDs||tctl00$ctl00$MainEditable$mainContent$UpdatePanel1,|0|childUpdatePanelIDs|||50|panelsToRefreshIDs||ctl00$ctl00$MainEditable$mainContent$UpdatePanel1,|2|asyncPostBackTimeout||90|13|formAction||./docket.aspx|50|pageTitle||Docket Search - Supreme Court of the United States|68|scriptStartupBlock|ScriptContentNoTags|window.__TsmHiddenField = $get('ctl00_ctl00_RadScriptManager1_TSM');| \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_granted_noted.html b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_granted_noted.html new file mode 100644 index 000000000..e33fd45e3 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_granted_noted.html @@ -0,0 +1,636 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Granted/Noted Cases List + + + + +
+
+ + + + +
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+
+ +
+ +
+ Search +
+
+
+
+
+
+ + +
+
+
+
+ Skip Navigation LinksHome > Case Documents > Granted/Noted Cases List +
+
+
+
+
+
+ + + +
+ + +
+

Granted/Noted Cases List

+
+
+ + +


+ + + +
+

 

+ + +
+ + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_journal.html b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_journal.html new file mode 100644 index 000000000..dc34ed478 --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_journal.html @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Journal - Supreme Court of the United States + + + + +
+
+ + + + +
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+
+ +
+ +
+ Search +
+
+
+
+
+
+ + +
+
+
+
+ Skip Navigation LinksHome > Case Documents > Journal +
+
+
+
+
+
+ + + +
+ + +
+

Journal

+
+
+ +
+

The Journal of the Supreme Court of the United States contains the official minutes of the Court. It is published chronologically for each day the Court +issues orders or opinions or holds oral argument. The Journal reflects the disposition of each case, names the court whose judgment is under review, lists +the cases argued that day and the attorneys who presented oral argument, contains miscellaneous announcements by the Chief Justice from the Bench, and sets +forth the names of attorneys admitted to the Bar of the Supreme Court. It does not contain opinions of the Court, which are published in the United States +Reports and in the "Opinions" section of this website.

+

New Journal entries are posted on this website about two weeks after the event.

+
+
+
+ +
+
+
+
+
+
+ +
+

 

+ + +
+ + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_orders_home.html b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_orders_home.html new file mode 100644 index 000000000..cd8cf821b --- /dev/null +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_orders_home.html @@ -0,0 +1,966 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Orders of the Court: Term Year 2023 + + + + +
+
+ + + + +
+ + + + + + + + + + + + + +
+ + + +
+
+ + +
+ +
+
+
+
+
+ Supreme Court of the United States +
+
+ +
+ +
+ Search +
+
+
+
+
+
+ + +
+
+
+
+ Skip Navigation LinksHome > Case Documents > Orders of the Court +
+
+
+
+
+
+ + + +
+ + +
+

Orders of the Court - Term Year 2023

+
+
+ + +
+

The vast majority of cases filed in the Supreme Court are disposed of summarily by unsigned orders. Such an order will, for example, deny a petition for certiorari without comment. Regularly scheduled lists of orders are issued on each Monday that the Court sits, but "miscellaneous" orders may be issued in individual cases at any time. Scheduled order lists are posted on this Website on the day of their issuance, while miscellaneous orders are posted on the day of issuance or the next day.

+

Caution: These electronic orders may contain computer-generated errors or other deviations from the official printed versions. Moreover, all order lists and miscellaneous orders are replaced within a few months by paginated versions of them in a preliminary print of the United States Reports, and one year after the issuance of the preliminary print by the final version of the orders in a U. S. Reports bound volume. In case of discrepancies between the print and electronic versions of orders, the print version controls. In case of discrepancies between order lists or miscellaneous orders and any later official version of them, the later version controls.

+
+
+ + + + +
+
+ +
+ + 2023 + + 2022 + + 2021 + + 2020 + + 2019 + + 2018 + + 2017 + + 2016 + + 2015 + + 2014 + + 2013 + + 2012 + + 2011 + + 2010 + +
+ +
+
 
+
+
+
+
+
+
+

+ Term Year: 2023 +

+
+
+
+ +
+ 03/25/24   + Order List +
+ +
+ 03/20/24   + Miscellaneous Order +
+ +
+ 03/20/24   + Miscellaneous Order +
+ +
+ 03/18/24   + Miscellaneous Order +
+ +
+ 03/18/24   + Miscellaneous Order +
+ +
+ 03/18/24   + Order List +
+ +
+ 03/15/24   + Miscellaneous Order +
+ +
+ 03/15/24   + Miscellaneous Order +
+ +
+ 03/12/24   + Miscellaneous Order +
+ +
+ 03/12/24   + Miscellaneous Order +
+ +
+ 03/06/24   + Miscellaneous Order +
+ +
+ 03/04/24   + Miscellaneous Order +
+ +
+ 03/04/24   + Miscellaneous Order +
+ +
+ 03/04/24   + Order List +
+ +
+ 02/28/24   + Miscellaneous Order +
+ +
+ 02/28/24   + Miscellaneous Order +
+ +
+ 02/28/24   + Miscellaneous Order +
+ +
+ 02/28/24   + Miscellaneous Order +
+ +
+ 02/26/24   + Order List +
+ +
+ 02/22/24   + Miscellaneous Order +
+ +
+
+
+
+ +
+
+

+ + More +

+
+
+
+
+ +
+ 02/20/24   + Order List +
+ +
+ 02/16/24   + Miscellaneous Order +
+ +
+ 02/16/24   + Miscellaneous Order +
+ +
+ 02/02/24   + Miscellaneous Order +
+ +
+ 02/02/24   + Miscellaneous Order +
+ +
+ 01/26/24   + Miscellaneous Order +
+ +
+ 01/24/24   + Miscellaneous Order +
+ +
+ 01/22/24   + Miscellaneous Order +
+ +
+ 01/22/24   + Miscellaneous Order +
+ +
+ 01/22/24   + Order List +
+ +
+ 01/16/24   + Order List +
+ +
+ 01/12/24   + Miscellaneous Order +
+ +
+ 01/08/24   + Order List +
+ +
+ 01/05/24   + Miscellaneous Order +
+ +
+ 01/05/24   + Miscellaneous Order +
+ +
+ 01/05/24   + Miscellaneous Order +
+ +
+ 12/22/23   + Miscellaneous Order +
+ +
+ 12/20/23   + Miscellaneous Order +
+ +
+ 12/14/23   + Miscellaneous Order +
+ +
+ 12/13/23   + Miscellaneous Order +
+ +
+ 12/11/23   + Miscellaneous Order +
+ +
+ 12/11/23   + Order List +
+ +
+ 12/08/23   + Miscellaneous Order +
+ +
+ 12/01/23   + Miscellaneous Order +
+ +
+ 11/27/23   + Miscellaneous Order +
+ +
+ 11/21/23   + Miscellaneous Order +
+ +
+ 11/20/23   + Order List +
+ +
+ 11/16/23   + Miscellaneous Order +
+ +
+ 11/16/23   + Miscellaneous Order +
+ +
+ 11/16/23   + Miscellaneous Order +
+ +
+ 11/16/23   + Miscellaneous Order +
+ +
+ 11/13/23   + Order List +
+ +
+ 11/09/23   + Miscellaneous Order +
+ +
+ 11/07/23   + Miscellaneous Order +
+ +
+ 11/06/23   + Order List +
+ +
+ 11/03/23   + Miscellaneous Order +
+ +
+ 10/30/23   + Order List +
+ +
+ 10/16/23   + Miscellaneous Order +
+ +
+ 10/16/23   + Order List +
+ +
+ 10/13/23   + Miscellaneous Order +
+ +
+ 10/13/23   + Miscellaneous Order +
+ +
+ 10/12/23   + Miscellaneous Order +
+ +
+ 10/10/23   + Miscellaneous Order +
+ +
+ 10/10/23   + Miscellaneous Order +
+ +
+ 10/10/23   + Order List +
+ +
+ 10/06/23   + Miscellaneous Order +
+ +
+ 10/02/23   + Miscellaneous Order +
+ +
+ 10/02/23   + Miscellaneous Order +
+ +
+ 10/02/23   + Order List +
+ +
+
+
+
+ +
+
+
+
+ +


+ +
+

 

+ + +
+ + + +
+ +
+
+
+ SUPREME COURT OF THE UNITED STATES + + 1 First Street, NE + + Washington, DC 20543 +
+ +
+ + + + + + + + +
+ + + \ No newline at end of file diff --git a/tests/local/test_ScotusClientTest.py b/tests/local/test_ScotusClientTest.py new file mode 100644 index 000000000..2b0108e83 --- /dev/null +++ b/tests/local/test_ScotusClientTest.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python + +import os +import unittest + +from juriscraper.lib.test_utils import MockResponse +from juriscraper.dockets.united_states.federal_appellate.scotus import clients +from tests import TESTS_ROOT_EXAMPLES_SCOTUS + + +class YAMockResponse(MockResponse): + """Mock a Request Response""" + + def __init__( + self, + status_code, + content=None, + headers=None, + request=None, + url=None, + reason: bytes = None, + ): + self.status_code = status_code + self._content = content + self.headers = headers + self.request = request + self.encoding = "utf-8" + self.reason = reason + self.url = url + + +class ScotusClientTest(unittest.TestCase): + """Test the download client shared by SCOTUS modules.""" + + def setUp(self): + + with open( + os.path.join(TESTS_ROOT_EXAMPLES_SCOTUS, "scotus_23-175.json"), + "rb", + ) as _json: + self.valid_docket = _json.read() + + self.docket_response = YAMockResponse( + status_code=200, + content=self.valid_docket, + headers={ + "content-length": str(len(self.valid_docket)), + "content-type": "application/json", + "last-modified": "Fri, 25 Aug 2023 18:01:01 GMT", + }, + ) + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, "scotus_access_denied.html" + ), + "rb", + ) as _json: + self.access_denied = _json.read() + + self.access_denied_response = YAMockResponse( + status_code=200, + content=self.access_denied, + url="https://www.supremecourt.gov/RSS/Cases/JSON/22A1059.json", + headers={ + "content-length": str(len(self.access_denied)), + "content-type": "text/html", + "last-modified": "Fri, 25 Aug 2023 18:02:02 GMT", + }, + ) + + self.code304_response = YAMockResponse( + status_code=304, + content="", + headers={ + "Content-Length": "0", + }, + ) + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, "scotus_JSON_not_found.html" + ), + "rb", + ) as _html: + self.not_found_page = _html.read() + + self.not_found_response = YAMockResponse( + status_code=200, + content=self.not_found_page, + headers={ + "content-length": str(len(self.not_found_page)), + "content-type": "text/html", + "last-modified": "Tue, 4 Jul 2023 18:01:01 GMT", + }, + ) + # TODO: mock the ConnectionError + # self.nre = clients.NameResolutionError + # self.ce = clients.ConnectionError + # self.ce.__context__ = self.nre + # self.ce_response = YAMockResponse( + # status_code=504, content=None, reason="Gateway Time-out" + # ) + # self.ce_response.url = "URL" + # self.ade_response = clients.AccessDeniedError + + def test_not_found_regex(self): + """Search example 'Not Found' page""" + nfp_text = self.not_found_page.decode() + valid_text = self.valid_docket.decode() + self.assertTrue(clients.not_found_regex.search(nfp_text)) + self.assertFalse(clients.not_found_regex.search(valid_text)) + + def test_not_found_test(self): + """Test example 'Not Found' page text""" + self.assertTrue(clients._not_found_test(self.not_found_response.text)) + are_false = ( + self.docket_response, + self.code304_response, + self.access_denied_response, + ) + for r in are_false: + with self.subTest(r=r): + self.assertFalse(clients._not_found_test(r.text)) + + def test_is_not_found_page(self): + """Test example 'Not Found' page text in Response""" + self.assertTrue(clients.is_not_found_page(self.not_found_response)) + are_false = ( + self.docket_response, + self.code304_response, + self.access_denied_response, + ) + for r in are_false: + with self.subTest(r=r): + self.assertFalse(clients.is_not_found_page(r)) + + def test_is_stale_content(self): + """Handle content whose 'Last-Updated' header is earlier than + the 'If-Updated-Since' header sent in the download request. + """ + self.assertTrue(clients.is_stale_content(self.code304_response)) + are_false = ( + self.docket_response, + self.not_found_response, + self.access_denied_response, + ) + for r in are_false: + with self.subTest(r=r): + self.assertFalse(clients.is_stale_content(r)) + + def test_access_denied_test(self): + """Test example 'Access Denied' page text""" + self.assertTrue( + clients._access_denied_test(self.access_denied_response.text) + ) + are_false = ( + self.docket_response, + self.code304_response, + self.not_found_response, + ) + for r in are_false: + with self.subTest(r=r): + self.assertFalse(clients._access_denied_test(r.text)) + + def test_is_access_denied_page(self): + """Test example 'Access Denied' page text in Response""" + self.assertTrue( + clients.is_access_denied_page(self.access_denied_response) + ) + are_false = ( + self.docket_response, + self.code304_response, + self.not_found_response, + ) + for r in are_false: + with self.subTest(r=r): + self.assertFalse(clients.is_access_denied_page(r)) + + def test_is_docket(self): + """Check for a docket JSON representation. Does not validate the JSON + itself.""" + self.assertTrue(clients.is_docket(self.docket_response)) + are_false = ( + self.access_denied_response, + self.code304_response, + self.not_found_response, + ) + for r in are_false: + with self.subTest(r=r): + self.assertFalse(clients.is_docket(r)) + + def test_jitter(self): + """A float between zero and the number passed.""" + test_value = 42 + test_result = clients.jitter(test_value) + self.assertGreaterEqual( + test_result, + 0, + ) + self.assertLessEqual(test_result, test_value) + self.assertIsInstance(test_result, float) + + def test_random_ua(self): + """It's a string. A User-Agent string.""" + result = clients.random_ua() + all_ua = [r["ua"] for r in clients.AGENTS] + self.assertIsInstance(result, str) + self.assertIn(result, all_ua) + + # TODO: mock the ConnectionError + def test_response_handler_connection_error(self): + """Handle exceptions from server-side measures.""" + pass + # with self.assertRaises(clients.ConnectionError): + # clients.response_handler(self.ce_response) + + # TODO: mock an exception that is not explicitly handled + def test_response_handler_uncaught_error(self): + """Logging for uncaught exceptions.""" + pass + # with self.assertRaises(RuntimeError): + # clients.response_handler(RuntimeError("uncaught")) + + def test_response_handler_access_denied_error(self): + """Handle 'Access Denied' page response.""" + with self.assertRaises(clients.AccessDeniedError): + clients.response_handler(self.access_denied_response) + + def test_response_handler_not_exceptions(self): + """Pass through inoffensive responses.""" + self.assertTrue(clients.is_docket(self.docket_response)) + should_pass = ( + self.docket_response, + self.code304_response, + self.not_found_response, + ) + for r in should_pass: + with self.subTest(r=r): + self.assertEqual(r, clients.response_handler(r)) diff --git a/tests/local/test_ScotusDocketSearchTest.py b/tests/local/test_ScotusDocketSearchTest.py new file mode 100644 index 000000000..6f5936e8f --- /dev/null +++ b/tests/local/test_ScotusDocketSearchTest.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python + +import datetime +import os +import unittest + +from juriscraper.lib.test_utils import MockResponse +from juriscraper.dockets.united_states.federal_appellate.scotus import ( + docket_search as ds, +) +from tests import TESTS_ROOT_EXAMPLES_SCOTUS + + +class YAMockResponse(MockResponse): + """Mock a Request Response""" + + def __init__( + self, + status_code, + content=None, + headers=None, + request=None, + url=None, + reason: bytes = None, + ): + self.status_code = status_code + self._content = content + self.headers = headers + self.request = request + self.encoding = "utf-8" + self.reason = reason + self.url = url + + +class ScotusOrdersTest(unittest.TestCase): + """Test the SCOTUS Orders of the Court manager and parser.""" + + def setUp(self): + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, "scotus_orders_home.html" + ), + "rb", + ) as _hp: + self.orders_home = _hp.read() + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, "orders_list_20240220.pdf" + ), + "rb", + ) as _ol: + self.orders_list_pdf = _ol.read() + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, "misc_order_20240222.pdf" + ), + "rb", + ) as _mo: + self.misc_order_pdf = _mo.read() + + self.pdf_links = ( + "https://www.supremecourt.gov/orders/courtorders/032024zr1_olp1.pdf", + "https://www.supremecourt.gov/orders/courtorders/032524zor_4h25.pdf", + ) + + def test_instance(self): + """Instantiate SCOTUSOrders""" + test_instance = ds.SCOTUSOrders(2023) + self.assertIsInstance(test_instance, ds.SCOTUSOrders) + + def test_order_url_regex(self): + """Extracts the date an order was published from its URI.""" + expected = (("03", "20", "24", "zr"), ("03", "25", "24", "zor")) + + for i, uri in enumerate(self.pdf_links): + with self.subTest(uri=uri): + match_obj = ds.order_url_date_regex.search(uri) + self.assertTrue(match_obj) + self.assertTrue(len(match_obj.groups()) == 4) + self.assertTupleEqual(expected[i], match_obj.groups()) + + def test_order_url_regex_fed_rules(self): + """Identify URI of federal judicial rules orders that we don't want.""" + URIs = ( + "https://www.supremecourt.gov/orders/courtorders/frap23_qol1.pdf", + "https://www.supremecourt.gov/orders/courtorders/frbk23_4315.pdf", + "https://www.supremecourt.gov/orders/courtorders/frcv23_3eah.pdf", + "https://www.supremecourt.gov/orders/courtorders/frcr23_d1pf.pdf", + "https://www.supremecourt.gov/orders/courtorders/frev23_5468.pdf", + ) + for uri in URIs: + with self.subTest(uri=uri): + match_obj = ds.fedrules_regex.search(uri) + self.assertTrue(match_obj) + + def test_orders_link_parser(self): + """Take an HTML string from the Orders page and return a list of Order URLs.""" + instance = ds.SCOTUSOrders(2023) + result = instance.orders_link_parser(self.orders_home) + self.assertIsInstance(result, list) + for uri in self.pdf_links: + # these URIs were taken from the 2023 term Orders home page + with self.subTest(uri=uri): + self.assertIn(uri, result) + + def test_parse_orders_page(self): + """Extract URLs for individual order documents.""" + expected_url = "https://www.supremecourt.gov/orders/courtorders/032524zor_4h25.pdf" + instance = ds.SCOTUSOrders(2023) + response = YAMockResponse( + status_code=200, headers={}, content=self.orders_home + ) + # patch instance attribute + instance.homepage_response = response + self.assertIsInstance(instance.homepage_response.text, str) + # run parser + instance.parse_orders_page() + self.assertNotEqual(instance.order_meta, []) + self.assertEqual(instance.order_meta[0]["url"], expected_url) + self.assertEqual(len(instance.order_meta), 69) + + def test_order_pdf_parser(self): + """Extract docket numbers from an Orders PDF.""" + instance = ds.SCOTUSOrders(2023) + expected1 = ("23-467", "23M49", "22-7860", "22-7871") + + result_misc = instance.order_pdf_parser(self.misc_order_pdf) + self.assertTrue(result_misc == {"23A741"}) + result_list = instance.order_pdf_parser(self.orders_list_pdf) + for docketnum in expected1: + with self.subTest(docketnum=docketnum): + self.assertIn(docketnum, result_list) + + def test_term_constraints(self): + """Allows fine-tuning of which Order dates to download from.""" + test_terms = (2023, 2023, 2022) + test_dates = ( + dict(earliest=None, latest=None), + dict(earliest="2024-02-28", latest="2023-11-30"), + dict(earliest="2022-09-28", latest="2023-11-30"), + ) + expected = ( + {"earliest": None, "latest": None}, + { + "earliest": datetime.date(2023, 11, 30), + "latest": datetime.date(2024, 2, 28), + }, + { + "earliest": datetime.date(2022, 10, 3), + "latest": datetime.date(2023, 10, 1), + }, + ) + for t, exp, kargs in zip(test_terms, expected, test_dates): + with self.subTest(t=t, exp=exp, kargs=kargs): + instance = ds.SCOTUSOrders(t) + result = instance._term_constraints(**kargs) + self.assertDictEqual(exp, result) + + +class ScotusDocketFullTextSearchTest(unittest.TestCase): + """Test the SCOTUS Docket Search manager and parser.""" + + def setUp(self): + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, + "scotus_docket_search_home.aspx", + ), + "r", + ) as _f: + self.homepage = _f.read() + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, + "scotus_docket_search_results_page1.aspx", + ), + "r", + ) as _f: + self.page1 = _f.read() + + with open( + os.path.join( + TESTS_ROOT_EXAMPLES_SCOTUS, + "scotus_docket_search_results_page2.aspx", + ), + "r", + ) as _f: + self.page2 = _f.read() + + self.test_instance = ds.DocketFullTextSearch("") + + def test_instance(self): + """Instantiate DocketFullTextSearch""" + self.assertIsInstance(self.test_instance, ds.DocketFullTextSearch) + + def test_classmethod_instantiation(self): + """Instantiate DocketFullTextSearch using the `date_query` classmethod. + This is the preferred method of instantiation.""" + instance = ds.DocketFullTextSearch.date_query("2024-04-01") + self.assertIsInstance(instance, ds.DocketFullTextSearch) + self.assertEqual(instance.search_string, "Apr 01, 2024") + + def test_search_page_metadata_parser(self): + """Parses hidden fields from Docket Search page.""" + expected = { + "ctl00_ctl00_RadScriptManager1_TSM": "", + "__EVENTTARGET": "", + "__EVENTARGUMENT": "", + "__VIEWSTATE": "jkF4BYmzedCwenqv/sgNCVMXPauykip5wr1PKLEeK6c+FmoDUAqrCoqJc0jApuDzRjFl/iDlmwlcFBsgh165V+fwuqfo5fgTp3J/0Iy/0+e/ovzd1kt2jh0YAL6J6qA+", + "__VIEWSTATEGENERATOR": "71826567", + } + meta_elements = self.test_instance.search_page_metadata_parser( + self.homepage + ) + self.assertDictEqual(meta_elements, expected) + + def test_page_count_parser1(self): + """Take an HTML string from the docket search results.""" + expected = {"items": 499, "cur_pg": 1, "max_pg": 100} + meta_elements = self.test_instance.page_count_parser(self.page1) + self.assertDictEqual(meta_elements, expected) + + def test_page_count_parser2(self): + """Take an HTML string from the docket search results.""" + expected = {"items": 499, "cur_pg": 2, "max_pg": 100} + meta_elements = self.test_instance.page_count_parser(self.page2) + self.assertDictEqual(meta_elements, expected) + + def test_docket_number_parser(self): + """Take an HTML string from the docket search results and return + a list of docket numbers.""" + expected1 = {"23-779", "23-726", "23-156", "23A769", "23-367"} + expected2 = {"23-6803", "23A481", "22O141", "23-250", "23-463"} + docket_numbers1 = self.test_instance.docket_number_parser(self.page1) + docket_numbers2 = self.test_instance.docket_number_parser(self.page2) + self.assertSetEqual(docket_numbers1, expected1) + self.assertSetEqual(docket_numbers2, expected2) diff --git a/tests/network/test_ScotusClientTest.py b/tests/network/test_ScotusClientTest.py new file mode 100644 index 000000000..12a433c2c --- /dev/null +++ b/tests/network/test_ScotusClientTest.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python + +import datetime +import unittest + +from juriscraper.lib.test_utils import MockResponse +from juriscraper.dockets.united_states.federal_appellate.scotus import ( + clients, + scotus_docket, + docket_search as ds, +) + + +class YAMockResponse(MockResponse): + """Mock a Request Response""" + + def __init__( + self, + status_code, + content=None, + headers=None, + request=None, + url=None, + reason: bytes = None, + ): + self.status_code = status_code + self._content = content + self.headers = headers + self.request = request + self.encoding = "utf-8" + self.reason = reason + self.url = url + + +@unittest.skip("All URIs passed on 2024-03-28") +class ScotusClientTest(unittest.TestCase): + """Test the download client shared by SCOTUS modules.""" + + def test_valid_uri_downloads(self): + """Known good URIs as of 2024-03-28.""" + URIs = ( + "https://www.supremecourt.gov/", + "https://www.supremecourt.gov/RSS/Cases/JSON/23-181.json", + "https://www.supremecourt.gov/DocketPDF/23/23-181/276036/20230823151847724_Word%20Count%20Petition%20for%20Writ%20Harrison.pdf", + "https://www.supremecourt.gov/docket/docket.aspx", + "https://www.supremecourt.gov/orders/ordersofthecourt/", + ) + for uri in URIs: + with self.subTest(uri=uri): + response = clients.download_client(uri) + self.assertTrue(response.ok) + + +@unittest.skip("All URIs passed on 2024-03-29") +class ScotusDownloadManagerTest(unittest.TestCase): + """Test the download managers.""" + + def setUp(self): + self.test_docket_numbers = [ + "23-175", # Grant's Pass; lots of entries + "23A1", # Simple application docket + "23M1", # Motion denied on first day of term + "22O141", # An 'Orig.' case; not many of these + "22-6592", # JSON missing final docket entry 2023-02-21 + ] + + def test_valid_docket_downloads(self): + """Known good docket numbers as of 2024-03-29.""" + responses = scotus_docket.linear_download(self.test_docket_numbers) + for i, r in enumerate(responses): + with self.subTest(r=r): + response = r + self.assertTrue(response.ok) + docket = response.json() + case_number = docket.get("CaseNumber") + self.assertEqual( + self.test_docket_numbers[i], case_number.rstrip() + ) + + +@unittest.skip("All passed on 2024-03-29") +class ScotusOrdersDownloadTest(unittest.TestCase): + """Test the Orders of the Court download managers.""" + + def setUp(self): + self.sample_order_pdfs = ( + "https://www.supremecourt.gov/orders/courtorders/032024zr1_olp1.pdf", + "https://www.supremecourt.gov/orders/courtorders/032524zor_4h25.pdf", + ) + + def test_orders_page_download(self): + """Download https://www.supremecourt.gov/orders/ordersofthecourt/""" + instance = ds.SCOTUSOrders(2023, cache_pdfs=False) + instance.orders_page_download() + self.assertIsNotNone(instance.homepage_response) + self.assertIsInstance( + instance.homepage_response, clients.requests.Response + ) + + def test_orders_page_download_304(self): + """Try the Orders home page with the 'If-Modified-Since' header set.""" + instance = ds.SCOTUSOrders(2023, cache_pdfs=False) + instance.orders_page_download( + since_timestamp=datetime.datetime.now(tz=datetime.timezone.utc) + ) + self.assertIsNotNone(instance.homepage_response) + self.assertIsInstance( + instance.homepage_response, clients.requests.Response + ) + # this should only fail if page is updated as query executes + self.assertTrue(instance.homepage_response.status_code == 304) + + def test_order_pdf_download(self): + """Download individual PDFs.""" + instance = ds.SCOTUSOrders(2023, cache_pdfs=False) + for uri in self.sample_order_pdfs: + with self.subTest(uri=uri): + response = instance.order_pdf_download(uri) + self.assertTrue(response.content[:4] == b"%PDF") + + def test_get_orders(self): + """Orders scraping manager.""" + instance = ds.SCOTUSOrders(2023, cache_pdfs=True) + kargs = dict( + earliest="2024-03-06", + latest="2024-03-06", + include_misc=True, + delay=0.25, + ) + # run manager method + instance._get_orders(**kargs) + # check side effects + self.assertIsNotNone(instance.homepage_response) + self.assertNotEqual(instance.order_meta, []) + self.assertIsNotNone(instance._pdf_cache) + self.assertTrue(tuple(instance._pdf_cache)[0].content[:4] == b"%PDF") + self.assertEqual(instance._docket_numbers, set(["23-411"])) + self.assertEqual(instance.docket_numbers(), ["23-411"]) + + +class ScotusFullTextSearchDownloadTest(unittest.TestCase): + """Test the Docket Search download managers.""" + + def setUp(self): + self.test_instance = ds.DocketFullTextSearch.date_query("2024-04-01") + + @unittest.skip("Passed on 2024-03-30") + def test_full_text_search_page(self): + """Download https://www.supremecourt.gov/docket/docket.aspx""" + response = self.test_instance.full_text_search_page() + self.assertIsInstance(response, clients.requests.Response) + self.assertTrue("Docket Search" in response.text) + + def test_search_query(self): + """Send POST full text search query. Use 2024-02-29 because there were + relatively few matches for the search term.""" + instance = ds.DocketFullTextSearch.date_query("2024-02-29") + instance.search_query() + self.assertIsNot(instance.query_responses, []) + self.assertIsInstance( + instance.query_responses[0], ds.requests.Response + ) + self.assertTrue("Docket Search" in instance.query_responses[0].text) + + # TODO: figure out mocking + def test_pager(self): + """Download 2,...,n pages of search results""" + pass + + def test_scrape(self): + """Scraping manager for Docket Search.""" + instance = ds.DocketFullTextSearch.date_query("2024-02-29") + instance.scrape() + for r in instance.query_responses: + with self.subTest(r=r): + self.assertIsInstance(r, ds.requests.Response) + self.assertTrue(r.ok) + self.assertTrue("Docket Search" in r.text) From e3b84d8d94a8f56a676959c86869e2794106f0f1 Mon Sep 17 00:00:00 2001 From: Rob Alexander <6756310+ralexx@users.noreply.github.com> Date: Sun, 31 Mar 2024 02:44:16 -0700 Subject: [PATCH 2/3] [WIP] SCOTUS docket parser --- .../federal_appellate/scotus/scotus_docket.py | 417 +++++++++++++++++- 1 file changed, 416 insertions(+), 1 deletion(-) diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py b/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py index 62223ab65..6a7efc2c2 100644 --- a/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py @@ -15,9 +15,11 @@ 'IFP' (in forma pauperis) a.k.a pauper cases number 5001 and up each term """ -# from concurrent.futures import ThreadPoolExecutor, as_completed +import json from math import sqrt +import re from time import sleep +from urllib.parse import urljoin import requests from requests.exceptions import ConnectionError @@ -121,3 +123,416 @@ def linear_download( _ss = sorted(list(stale_set), key=utils.docket_priority) logger.debug(f"Stale dockets: {_ss}") + + +judgment_regex = re.compile(r"^Judgment", flags=re.IGNORECASE) +simple_petition_regex = re.compile( + r"((?<=Petition\s)|(?<=Case\s))(DENIED|DISMISSED)", flags=re.IGNORECASE +) +affirmed_regex = re.compile(r"Adjudged to be AFFIRMED") +closed_regex = re.compile(r"Case considered closed") +removed_regex = re.compile(r"Case removed from Docket") +cert_dismissed_regex = re.compile(r"Writ of Certiorari Dismissed") + +# use this only after splitting entry text on '.' +wildcard_petition_regex = re.compile( + r"((?<=Petition\s)|(?<=Case\s)).*(DENIED|DISMISSED)", flags=re.IGNORECASE +) + + +denied_regex = re.compile( + r"(?<=Application )(?:\(\w{4,8}\)\s)?.*DENIED" + r"|(?<=Motion )(?:\(\w{4,8}\)\s)?.*DENIED", + flags=re.IGNORECASE, +) +ifp_dismissed_regex = re.compile( + r"Motion of petitioner for leave to proceed in forma pauperis denied, " + r"and petition for writ of (mandamus|certiorari to the).*dismissed." +) + + +# TODO: text value cleaning, harmonizing from lib.string_utils +class SCOTUSDocketReport: + """Parser for SCOTUS JSON dockets. + Modeled on juriscraper.pacer.appellate_docket.AppellateDocketReport. + """ + + meta_mapping = { + "docket_number": "CaseNumber", + "capital_case": "bCapitalCase", + "question_presented": "QPLink", # URL + "related_cases": "RelatedCaseNumber", # list type + "linked_cases": "Links", # str 'Linked with YY-NNNNN' + "fee_status": "sJsonCaseType", + "date_filed": "DocketedDate", + "petitioner": "PetitionerTitle", + "respondent": "RespondentTitle", + "lower_court": "LowerCourt", + "lower_court_case_numbers": "LowerCourtCaseNumbers", # list type? + "lower_court_decision_date": "LowerCourtDecision", + "lower_court_rehearing_denied_date": "LowerCourtRehearingDenied", + } + entries_mapping = { + "date_filed": "Date", + "description": "Text", + "document_title": "Description", + "url": "DocumentUrl", + } + + base_url = "https://www.supremecourt.gov/RSS/Cases/JSON/{}.json" + + # exclude docket boilerplate entries e.g. 'Proof of Service' + exclude_entries_pattern = r"Main Document" r"|Proof of Service" + exclude_entries_regex = re.compile(exclude_entries_pattern) + petitioner_regex = re.compile(r".+(?=, Petitioner|Applicant)") + + def __init__(self, docket: dict, **kwargs): + """Takes the decoded JSON object for a single SCOTUS docket.""" + self._docket = docket + self._kwargs = kwargs + self.court_id = self.__module__ + self._docket_number = None + self._url = utils.docket_number_regex.search(docket["CaseNumber"]) + self._metadata = None + self._docket_entries = None + self._attorneys = None + self._dispositions = [] + + @classmethod + def from_response(cls, response: requests.Response, **kwargs): + """Instantiate with values taken from `response`.""" + return cls.__init__(response.json(), **kwargs) + + @classmethod + def from_text(cls, json_text: str, **kwargs): + """Instantiate with dict from a JSON-encoded string.""" + return cls.__init__(json.loads(json_text), **kwargs) + + @property + def docket_number(self): + if self._docket_number is None: + dn = self._docket["CaseNumber"].rstrip() + self._docket_number = utils.docket_num_strict_regex.search( + dn + ).group(0) + return self._docket_number + + def _get_petitioner(self): + """Strip ', Petitioner' from the title as this is implicit.""" + _petitioner = self._docket["PetitionerTitle"] + if psearch := self.petitioner_regex.search(_petitioner): + _petitioner = psearch.group(0) + return _petitioner + + def _get_case_name(self): + """Petitioner v. Respondent""" + return f'{self._get_petitioner()} v. {self._docket["RespondentTitle"]}' + + def _get_lower_court_cases(self): + """These are presented as a string representation of a tuple, but not + correctly formatted for serializing. Use regex.""" + if (casenums := self._docket["LowerCourtCaseNumbers"]) == "": + return None + else: + return casenums.strip("()").replace(" ", "").split(",") + + def _get_related_cases(self): + """These are presented as a list.""" + # cases_str = + if (related := self._docket["RelatedCaseNumber"]) != []: + return related + else: + return None + + @property + def url(self): + url_template = "https://www.supremecourt.gov/RSS/Cases/JSON/{}.json" + return url_template.format(self.docket_number) + + def query(self, docket_number, since_timestamp, *args, **kwargs): + """Query supremecourt.gov and set self.response with the response.""" + raise NotImplementedError(".query() must be overridden") + # url = self.base_url.format(docket_number) + # response = download_client(url, since_timestamp=since_timestamp) + # validated_response = response_handler(response) + + # if is_stale_docket(validated_response): + # pass + # elif is_docket(validated_response): + # self._docket = validated_response.json() + + def parse(self): + """Parse the JSON data provided in a requests.response object. In most cases, you won't need to call this since it will be automatically called by self.query, if needed. + + :return: None + """ + self._parse_text(self._docket) + + def _parse_text(self, *args): + """A method intended to clean up HTML source. + + This is being preserved in case it is needed for adapting the JSON parser to a caller that expects the `BaseReport` interface. + + :return: None + """ + raise NotImplementedError("This class does not parse HTML text.") + + def _get_questions_presented(self): + """Download 'Questions Presented' PDF. Missing from some dockets, for + example, motions or applications for extension of time. + """ + qp = None + + if pdfpath := self._docket.get("QPLink"): + base = "https://www.supremecourt.gov/" + url = urljoin(base, pdfpath) + r = requests.get(url) + r.raise_for_status() + if r.headers.get("content-type") == "application/pdf": + msg = ( + "Got PDF binary data for 'Questions Presented' " + f"in {self.docket_number}" + ) + logger.info(msg) + qp = r + return qp + + def _parse_filing_links( + self, links: list, keep_boilerplate: bool = True + ) -> list: + """Return the main documents from a docket entry with links by excluding + ancillary documents e.g. 'Proof of Service' and 'Certificate of Word Count'. + + :param links: List of docket entries + """ + filings = [] + for row in links: + match = utils.filing_url_regex.search(row["DocumentUrl"]) + record = { + "entry_number": match.group("entrynum"), + "document_title": row["Description"], + "url": row["DocumentUrl"], + "document_timestamp": utils.parse_filing_timestamp( + match.group("timestamp") + ), + } + if not keep_boilerplate and self.exclude_entries_regex.search( + row["Description"] + ): + continue + filings.append(record) + return filings + + @property + def metadata(self) -> dict: + if self._metadata is None: + data = { + "petitioner": "PetitionerTitle", + "respondent": "RespondentTitle", + "appeal_from": "LowerCourt", + "question_presented": "QPLink", + "related_cases": "RelatedCaseNumber", + "linked_cases": "Links", # str 'Linked with YY-NNNNN' + "fee_status": "sJsonCaseType", + "date_filed": "DocketedDate", + "lower_court": "LowerCourt", + "lower_court_case_numbers": "LowerCourtCaseNumbers", # list type? + "lower_court_decision_date": "LowerCourtDecision", + "lower_court_rehearing_denied_date": "LowerCourtRehearingDenied", + "capital_case": "bCapitalCase", + } + self._metadata = { + "court_id": self.court_id, + "docket_number": self.docket_number, + "case_name": self._get_case_name(), + } + self._metadata |= {k: self._docket.get(v) for k, v in data.items()} + return self._metadata + + def _original_entries(self) -> list: + """The original docket entries from the docket JSON.""" + return self._docket["ProceedingsandOrder"] + + def _entry_count(self) -> int: + """The number of docket entries as originally presented on supremecourt.gov. + This may differ from the number of entries as parsed elsewhere in this class. + """ + return len(self._docket["ProceedingsandOrder"]) + + @property + def docket_entries(self) -> list: + """Return docket entries as a list of dict records. Where an entry contains multiple attachments, merge entry and attachment metadata.""" + if self._docket_entries is None: + docket_entry_rows = self._docket["ProceedingsandOrder"] + + docket_entries = [] + for row in docket_entry_rows: + de = { + "date_filed": utils.makedate(row["Date"]).date(), + "docket_number": self.docket_number, + "description": row["Text"], + } + + if links := row.get("Links"): + filing_list = self._parse_filing_links(links) + for filing in filing_list: + docket_entries.append(de | filing) + else: + docket_entries.append(de) + + self._docket_entries = docket_entries + return self._docket_entries + + # TODO: sometimes dispositions are subject to rehearing; abandon this? + @property + def disposition(self): + """Find any case disposition in docket entries.""" + if self._dispositions == []: + + def mkdt(entry): + """Return a datetime.date object from the entry's date field.""" + return utils.makedate(entry["Date"]).date() + + for entry in self._original_entries(): + desc = entry["Text"] + # Judgment issued + if judgment_regex.search(desc) or affirmed_regex.search(desc): + self._dispositions.append(("DECIDED", mkdt(entry))) + continue + # Petition denied or dismissed + elif _disposition := simple_petition_regex.search(desc): + self._dispositions.append( + (_disposition.group(2).upper(), mkdt(entry)) + ) + continue + elif closed_regex.search(desc): + self._dispositions.append(("CLOSED", mkdt(entry))) + continue + elif removed_regex.search(desc): + self._dispositions.append(("REMOVED", mkdt(entry))) + continue + elif cert_dismissed_regex.search(desc): + self._dispositions.append(("DISMISSED", mkdt(entry))) + continue + elif _related := self._get_related_cases(): + # could be Vided (i.e. combined) + for row in _related: + if isinstance(row, dict): + if "Vide" in row["RelatedType"]: + self._dispositions.append( + ("VIDED", mkdt(entry)) + ) + break + + # now try splitting field on period before pattern matching + for s in desc.split("."): + sentence = s.strip() + if sentence == "": + continue + elif judgment_regex.search( + sentence + ) or affirmed_regex.search(sentence): + self._dispositions.append(("DECIDED", mkdt(entry))) + break + elif _disposition := wildcard_petition_regex.search( + sentence + ): + self._dispositions.append( + (_disposition.group(2).upper(), mkdt(entry)) + ) + break + + # elif ifp_dismissed_regex.search(desc): + # self._dispositions.append( + # ("DISMISSED", utils.makedate(entry["Date"]).date()) + # ) + # elif denied_regex.search(desc): + # self._dispositions.append( + # ("DENIED", utils.makedate(entry["Date"]).date()) + # ) + # elif dismissed_regex.search(desc): + # self._dispositions.append( + # ("DISMISSED", utils.makedate(entry["Date"]).date()) + # ) + try: + return self._dispositions[-1] + except IndexError: + return None + + @staticmethod + def _parse_attorney(row, affiliation) -> dict: + """Parse an attorney entry.""" + record = { + "affiliation": affiliation, + "party": row["PartyName"], + "counsel_of_record": row["IsCounselofRecord"], + "name": row["Attorney"], + "firm": row["Title"], + "prisoner_id": row["PrisonerId"], + "contact": { + "email": row["Email"], + "phone": row["Phone"], + "address": row["Address"], + "city": row["City"], + "state": row["State"], + "zipcode": row["Zip"], + }, + } + return record + + def _parse_petitioner_attorneys(self) -> list: + """Parse attorney(s) for Petitioner.""" + records = [] + for row in self._docket["Petitioner"]: + row["docket_number"] = self._docket_number + records.append(self._parse_attorney(row, "Petitioner")) + return records + + def _parse_respondent_attorneys(self) -> list: + """Parse attorney(s) for Respondent.""" + records = [] + if self._docket.get("Respondent"): + for row in self._docket["Respondent"]: + row["docket_number"] = self._docket_number + records.append(self._parse_attorney(row, "Respondent")) + return records + + def _parse_other_attorneys(self) -> list: + """Parse attorney(s) representing other parties, if any.""" + records = [] + if self._docket.get("Other"): + for row in self._docket["Other"]: + row["docket_number"] = self._docket_number + records.append(self._parse_attorney(row, "Other")) + return records + + def _parse_attorneys(self): + """Parse all attorneys for this docket.""" + all_attorneys = ( + self._parse_petitioner_attorneys(), + self._parse_respondent_attorneys(), + self._parse_other_attorneys(), + ) + return [r for party in all_attorneys for r in party] + + @property + def parties(self) -> list: + """Modeled on juriscraper.pacer.docket_report.DocketReport.""" + petitioner = { + "name": self._get_petitioner(), + "type": "Petitioner", + "attorneys": self._parse_petitioner_attorneys(), + } + respondent = { + "name": self._docket["RespondentTitle"], + "type": "Respondent", + "attorneys": self._parse_respondent_attorneys(), + } + return [petitioner, respondent] + + @property + def attorneys(self) -> list: + """List of all attorneys associated with this docket.""" + if self._attorneys is None: + self._attorneys = self._parse_attorneys() + return self._attorneys From 56a9c58fb497548f77dd5d834071af166a7c1335 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:37:11 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../federal_appellate/scotus/clients.py | 16 +-- .../federal_appellate/scotus/docket_search.py | 62 +++++----- .../federal_appellate/scotus/scotus_docket.py | 12 +- .../federal_appellate/scotus/utils.py | 31 +++-- tests/__init__.py | 1 + .../scotus/scotus_docket_search_home.aspx | 88 +++++++------- .../scotus_docket_search_results_page1.aspx | 114 +++++++++--------- .../scotus_docket_search_results_page2.aspx | 38 +++--- tests/local/test_ScotusClientTest.py | 3 +- tests/local/test_ScotusDocketSearchTest.py | 7 +- tests/network/test_ScotusClientTest.py | 10 +- 11 files changed, 187 insertions(+), 195 deletions(-) diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py b/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py index 59c63491a..ba86c89d6 100644 --- a/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/clients.py @@ -1,18 +1,18 @@ """Download clients for supremecourt.gov""" -from random import random, choices import re +from random import choices, random +import requests from lxml import html from lxml.etree import ParserError -import requests from requests.exceptions import ConnectionError from urllib3.exceptions import NameResolutionError -from juriscraper.lib.log_tools import make_default_logger from juriscraper.lib.exceptions import AccessDeniedError -from . import utils +from juriscraper.lib.log_tools import make_default_logger +from . import utils logger = make_default_logger() @@ -128,9 +128,9 @@ def _access_denied_test(text: str) -> bool: def is_access_denied_page(response: requests.Response) -> bool: """Take an HTML string from a `Response.text` and test for `Access Denied`.""" - ct, cl = [ + ct, cl = ( response.headers.get(f) for f in ("content-type", "content-length") - ] + ) if ct and cl: if ct.startswith("text/html") and int(cl) > 0: return _access_denied_test(response.text) @@ -156,9 +156,9 @@ def is_not_found_page(response: requests.Response) -> bool: """Take an HTML string from a `Response.text` and test for `ERROR: File or directory not found.`. """ - ct, cl = [ + ct, cl = ( response.headers.get(f) for f in ("content-type", "content-length") - ] + ) if ct and cl: if ct.startswith("text/html") and int(cl) > 0: return _not_found_test(response.text) diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py b/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py index e90b5764e..03709959e 100644 --- a/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/docket_search.py @@ -3,9 +3,9 @@ > Paper remains the official form of filing at the Supreme Court. -Perhaps because of this second-millennium record keeping practice, there is no -publicly-available digital central index of Supreme Court cases. If SCOTUS docket -information is not obtained by attempting to query docket numbers in sequence, +Perhaps because of this second-millennium record keeping practice, there is no +publicly-available digital central index of Supreme Court cases. If SCOTUS docket +information is not obtained by attempting to query docket numbers in sequence, one of the Court's information sources needs to be consulted. In descending order of relevance to scraping, these are: @@ -15,54 +15,54 @@ [Journal of the SCOTUS](https://www.supremecourt.gov/orders/journal.aspx) **Orders of the Court** -* PDFs listing docket numbers of cases for which there has been an action. -> Regularly scheduled lists of orders are issued on each Monday that the Court -sits, but 'miscellaneous' orders may be issued in individual cases at any time. -Scheduled order lists are posted on this Website on the day of their issuance, +* PDFs listing docket numbers of cases for which there has been an action. +> Regularly scheduled lists of orders are issued on each Monday that the Court +sits, but 'miscellaneous' orders may be issued in individual cases at any time. +Scheduled order lists are posted on this Website on the day of their issuance, while miscellaneous orders are posted on the day of issuance or the next day." **Docket Search** -* Full-text search portal that attempts to match the input string to any text +* Full-text search portal that attempts to match the input string to any text in any docket. * Returns URLs of the HTML rendering for dockets. From these, docket numbers can be parsed and substituted into the URL pattern for dockets' JSON rendering. -* Passing date strings formatted like docket entry dates (e.g. 'Feb 29, 2024') -will with high probability return a link to a docket with an entry(s) matching +* Passing date strings formatted like docket entry dates (e.g. 'Feb 29, 2024') +will with high probability return a link to a docket with an entry(s) matching the input string. -* There appears to be a limit of 500 search results. Because some of the results -will be for matches in the text of a docket entry rather than the entry date +* There appears to be a limit of 500 search results. Because some of the results +will be for matches in the text of a docket entry rather than the entry date itself, it is possible that some search results will not be exhaustive. **Journal of the Supreme Court** * The exhaustive -- but not at all timely -- PDFs containing all docket numbers. -> The Journal of the Supreme Court of the United States contains the official -minutes of the Court. It is published chronologically for each day the Court -issues orders or opinions or holds oral argument. The Journal reflects the -disposition of each case, names the court whose judgment is under review, lists +> The Journal of the Supreme Court of the United States contains the official +minutes of the Court. It is published chronologically for each day the Court +issues orders or opinions or holds oral argument. The Journal reflects the +disposition of each case, names the court whose judgment is under review, lists the cases argued that day and the attorneys who presented oral argument[]. """ +import re from datetime import date, timedelta from io import BytesIO - from random import shuffle -import re from time import sleep import fitz # the package name of the `pymupdf` library -from lxml import html import requests +from lxml import html from juriscraper.lib.log_tools import make_default_logger + +from . import utils from .clients import ( + HEADERS, download_client, - response_handler, is_not_found_page, is_stale_content, jitter, - HEADERS, random_ua, + response_handler, ) -from . import utils logger = make_default_logger() @@ -203,7 +203,7 @@ def parse_orders_page(self) -> None: continue else: container["order_date"] = date( - int("20" + _yy), int(_mm), int(_dd) + int(f"20{_yy}"), int(_mm), int(_dd) ) container["order_type"] = listype container["url"] = url @@ -256,9 +256,7 @@ def order_pdf_parser(pdf: bytes) -> set: pdf_string = "\n".join([pg.get_text() for pg in _pdf]) matches = utils.orders_docket_regex.findall(pdf_string) # clean up dash characters so only U+002D is used - docket_set = set( - [utils.dash_regex.sub("\u002d", dn) for dn in matches] - ) + docket_set = {utils.dash_regex.sub("\u002d", dn) for dn in matches} return docket_set def _term_constraints(self, *, earliest: str, latest: str) -> dict: @@ -537,9 +535,9 @@ def search_query(self): _search_string = self.search_string # .replace(" ", "+") payload = self.post_template | hidden_payload - payload["ctl00$ctl00$MainEditable$mainContent$txtQuery"] = ( - _search_string - ) + payload[ + "ctl00$ctl00$MainEditable$mainContent$txtQuery" + ] = _search_string try: _response = self.session.post(self.SEARCH_URL, data=payload) @@ -588,9 +586,9 @@ def pager(self): # now update POST payload payload.update(hidden_payload) payload.update(payload_updates) - payload["ctl00$ctl00$MainEditable$mainContent$txtQuery"] = ( - self.search_string - ) + payload[ + "ctl00$ctl00$MainEditable$mainContent$txtQuery" + ] = self.search_string # extract __VIEWSTATE value from non-HTML text if vs_match := self.viewstate_regex.search( self.query_responses[-1].text diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py b/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py index 6a7efc2c2..e2f37a06a 100644 --- a/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/scotus_docket.py @@ -16,26 +16,26 @@ """ import json -from math import sqrt import re +from math import sqrt from time import sleep from urllib.parse import urljoin import requests from requests.exceptions import ConnectionError -from juriscraper.lib.log_tools import make_default_logger from juriscraper.lib.exceptions import AccessDeniedError +from juriscraper.lib.log_tools import make_default_logger + +from . import utils from .clients import ( download_client, - response_handler, - jitter, is_docket, is_not_found_page, is_stale_content, + jitter, + response_handler, ) -from . import utils - logger = make_default_logger() diff --git a/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py b/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py index b4513c2a5..cd3659011 100644 --- a/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py +++ b/juriscraper/dockets/united_states/federal_appellate/scotus/utils.py @@ -1,16 +1,15 @@ """Utilities for SCOTUS docket scraping and processing.""" +import re from datetime import date, datetime, timedelta from hashlib import sha1 -import re import dateutil.parser - """Text utilities -Note: PDF parsing can return various Unicode dashes beyond the standard -hyphen-minus character (U+002d). The docket search pattern uses U+002d, +Note: PDF parsing can return various Unicode dashes beyond the standard +hyphen-minus character (U+002d). The docket search pattern uses U+002d, U+2010, U+2011, U+2012, U+2013, and U+2014. """ @@ -23,13 +22,11 @@ # parse docket PDF filing URLs filing_url_regex = re.compile( - ( - r"(?<=www\.supremecourt\.gov)(?:/\w+?/)" - r"(?P\d\d)/" - r"(?P\d{2}[-AMO]\d{1,5})/" - r"(?P\d+)/" - r"(?P\d{17})" - ) + r"(?<=www\.supremecourt\.gov)(?:/\w+?/)" + r"(?P\d\d)/" + r"(?P\d{2}[-AMO]\d{1,5})/" + r"(?P\d+)/" + r"(?P\d{17})" ) @@ -47,10 +44,10 @@ def dedocket(docket_number: str) -> tuple: """Accept a padded docket string and return components of a docket number as a tuple e.g. (2023, '-', 5) or (2017, 'A', 54).""" - term, mod, casenum = [ + term, mod, casenum = ( docket_number[i:j] for i, j in ((0, 2), (2, 3), (3, 8)) - ] - return int("20" + term), mod.upper(), int(casenum) + ) + return int(f"20{term}"), mod.upper(), int(casenum) def padocket(x) -> str: @@ -66,10 +63,10 @@ def padocket(x) -> str: and docket_type.upper() in {"-", "A", "M", "O"} and isinstance(case_num, int) ) - return str(yyyy)[2:4] + docket_type + ("0000" + str(case_num))[-5:] + return str(yyyy)[2:4] + docket_type + f"0000{str(case_num)}"[-5:] elif isinstance(x, str): assert docket_number_regex.search(x) - return x[:3] + ("0000" + str(int(x[3:])))[-5:] + return x[:3] + f"0000{str(int(x[3:]))}"[-5:] def endocket(x) -> str: @@ -165,7 +162,7 @@ def parse_filing_timestamp(ts): ts[8:10], ts[10:12], ts[12:14], - ts[14:] + "000", + f"{ts[14:]}000", ) return datetime(*[int(x) for x in argz]) diff --git a/tests/__init__.py b/tests/__init__.py index 86d3a934e..59aad2ee3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -18,5 +18,6 @@ "scotus", ) + def test_local(): return unittest.TestLoader().discover("./tests/local") diff --git a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx index fb4b77d45..e56fed934 100644 --- a/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx +++ b/tests/examples/dockets/united_states/federal_appellate/scotus/scotus_docket_search_home.aspx @@ -10,10 +10,10 @@ - + - + - - + + - @@ -113,7 +113,7 @@ Sys.WebForms.PageRequestManager._initialize('ctl00$ctl00$RadScriptManager1', 'as
- + @@ -151,14 +151,14 @@ Sys.WebForms.PageRequestManager._initialize('ctl00$ctl00$RadScriptManager1', 'as +
@@ -251,49 +251,49 @@ Sys.WebForms.PageRequestManager._initialize('ctl00$ctl00$RadScriptManager1', 'as

- + - +
- +

Docket Search


-

The Supreme Court’s docket system contains information about cases, both pending and decided, that have been filed at the Court. The docket provided +

The Supreme Court’s docket system contains information about cases, both pending and decided, that have been filed at the Court. The docket provided here contains complete information regarding the status of cases filed since the beginning of the 2001 Term.

-

Users can search for the docket in a particular case by using a Supreme Court docket number, a case name, or other words or numbers included on a docket +

Users can search for the docket in a particular case by using a Supreme Court docket number, a case name, or other words or numbers included on a docket report. The format for Supreme Court docket numbers is "Term year-number" (e.g., 21-471; 22-5301).

-

Users can also sign up to receive email notifications of activity in pending cases. To do so, visit the docket page for an individual case and click on - the envelope icon that is just above the case number. You will be asked to enter an email address. When you click “Subscribe,” an email will be sent to - you with a link for you to confirm the correct email address. Once you click that link, you will receive email notifications every time there is a new +

Users can also sign up to receive email notifications of activity in pending cases. To do so, visit the docket page for an individual case and click on + the envelope icon that is just above the case number. You will be asked to enter an email address. When you click “Subscribe,” an email will be sent to + you with a link for you to confirm the correct email address. Once you click that link, you will receive email notifications every time there is a new filing or action by the Court in the case.

-

Questions Presented. The Questions Presented in a granted or noted case can be obtained by first obtaining the docket report for that case, then - clicking on the blue “Questions Presented” hyperlink located on the left side of the docket report. Once the hyperlink is clicked, a .pdf file setting +

Questions Presented. The Questions Presented in a granted or noted case can be obtained by first obtaining the docket report for that case, then + clicking on the blue “Questions Presented” hyperlink located on the left side of the docket report. Once the hyperlink is clicked, a .pdf file setting forth the Questions Presented in the case will appear.

@@ -307,35 +307,35 @@ Sys.WebForms.PageRequestManager._initialize('ctl00$ctl00$RadScriptManager1', 'as
- +
- +

- +    - +    - +    - +
- +
- +


Note: The Engrossed Dockets from 1791 to 1995 have been scanned by the National Archives from its microfilm collection and are available in its Catalog. - - - + + +


-
+