Skip to content

Commit

Permalink
replace opinion download function with client class
Browse files Browse the repository at this point in the history
  • Loading branch information
mscarey committed May 4, 2021
1 parent 675f624 commit 48f45ba
Show file tree
Hide file tree
Showing 9 changed files with 1,491 additions and 141 deletions.
12 changes: 9 additions & 3 deletions authorityspoke/decisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@


@dataclass
class CaseCitation:
class CAPCitation:
cite: str
reporter: Optional[str] = None


@dataclass
class CAPCitationTo:
cite: str
case_ids: List[int] = field(default_factory=list)


class Decision(Comparable):
r"""
A court decision to resolve a step in litigation.
Expand Down Expand Up @@ -74,13 +80,13 @@ def __init__(
date: datetime.date,
name: Optional[str] = None,
name_abbreviation: Optional[str] = None,
citations: Optional[Sequence[CaseCitation]] = None,
citations: Optional[Sequence[CAPCitation]] = None,
first_page: Optional[int] = None,
last_page: Optional[int] = None,
court: Optional[str] = None,
opinions: Optional[Union[Opinion, Sequence[Opinion]]] = None,
jurisdiction: Optional[str] = None,
cites_to: Optional[Sequence[CaseCitation]] = None,
cites_to: Optional[Sequence[CAPCitation]] = None,
id: Optional[int] = None,
) -> None:
self.date = date
Expand Down
236 changes: 142 additions & 94 deletions authorityspoke/io/downloads.py
Original file line number Diff line number Diff line change
@@ -1,100 +1,148 @@
"""
Downloading data that can be converted to authorityspoke objects.
"""
from __future__ import annotations
"""Downloading data that can be converted to authorityspoke objects."""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Union

import eyecite
from eyecite.models import CaseCitation
import requests


def download_case(
cap_id: Optional[int] = None,
cite: Optional[str] = None,
full_case: bool = False,
api_key: Optional[str] = None,
many: bool = False,
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
"""
Download cases from Caselaw Access Project API.
Queries the Opinion endpoint of the
`Caselaw Access Project API <https://api.case.law/v1/cases/>`_,
saves the JSON object(s) from the response to the
``example_data/cases/`` directory in the repo,
and returns one or more dict objects from the JSON.
:param cap_id:
an identifier for an opinion in the
`Caselaw Access Project database <https://case.law/api/>`_,
e.g. 4066790 for
`Oracle America, Inc. v. Google Inc. <https://api.case.law/v1/cases/4066790/>`_.
:param cite:
a citation linked to an opinion in the
`Caselaw Access Project database <https://case.law/api/>`_.
Usually these will be in the traditional format
``[Volume Number] [Reporter Name Abbreviation] [Page Number]``, e.g.
`750 F.3d 1339 <https://case.law/search/#/cases?page=1&cite=%22750%20F.3d%201339%22>`_
for Oracle America, Inc. v. Google Inc.
If the ``cap_id`` field is given, the cite field will be ignored.
If neither field is given, the download will fail.
:param full_case:
whether to request the full text of the opinion from the
`Caselaw Access Project API <https://api.case.law/v1/cases/>`_.
If this is ``True``, the `api_key` parameter must be
provided.
:param api_key:
a Caselaw Access Project API key. Visit
https://case.law/user/register/ to obtain one. Not needed if you
only want to download metadata about the opinion without the
full text.
:param always_list:
If True and as_generator is False, a single case from the API will
be returned as a one-item list. If False and as_generator is False,
a single case will be a list.
:returns:
a case record or list of case records from the API.
"""
endpoint = "https://api.case.law/v1/cases/"
params = {}
if cap_id:
endpoint += f"{cap_id}/"
elif cite is not None:
params["cite"] = cite
else:
raise ValueError(
"To identify the desired opinion, either 'cap_id' or 'cite' "
"must be provided."
)

api_dict = {}
if full_case:
if not api_key:
raise ValueError("A CAP API key must be provided when full_case is True.")
else:
api_dict["Authorization"] = f"Token {api_key}"

if full_case:
params["full_case"] = "true"
response = requests.get(endpoint, params=params, headers=api_dict).json()

if cap_id and response.get("detail") == "Not found.":
raise ValueError(f"API returned no cases with id {cap_id}")
if cite and not response.get("results") and response.get("results") is not None:
raise ValueError(f"API returned no cases with cite {cite}")

# Because the API wraps the results in a list only if there's
# more than one result.

if response.get("results"):
if many:
return response["results"]
return response["results"][0]
return response
from authorityspoke.decisions import CAPCitation
from authorityspoke.io.schemas_json import RawDecision


class AuthoritySpokeAPIError(Exception):
"""Error for invalid API query."""

pass


def normalize_case_cite(cite: Union[str, CaseCitation, CAPCitation]) -> str:
"""Get just the text that identifies a citation."""
if isinstance(cite, CAPCitation):
return cite.cite
if isinstance(cite, str):
possible_cites = list(eyecite.get_citations(cite))
bad_cites = []
for possible in possible_cites:
if isinstance(possible, CaseCitation):
return possible.base_citation()
bad_cites.append(possible)
error_msg = f"Could not locate a CaseCitation in the text {cite}."
for bad_cite in bad_cites:
error_msg += f" {str(bad_cite)} was a {bad_cite.__class__.__name__}, not a CaseCitation."
raise ValueError(error_msg)
return cite.base_citation()


class CAPClient:
"""Downloads Decisions from Case Access Project API."""

def __init__(self, api_token: str = ""):

"""Create download client with an API token and an API address."""
self.endpoint = f"https://api.case.law/v1/cases/"
if api_token and api_token.startswith("Token "):
api_token = api_token.split("Token ")[1]
self.api_token = api_token or ""

def get_api_headers(self, full_case=bool) -> Dict[str, str]:
"""Get API headers based on whether the full case text is requested."""
api_dict = {}
if full_case:
if not self.api_token:
raise AuthoritySpokeAPIError(
"To fetch full opinion text using the full_case parameter, "
"set the CAPClient's 'api_key' attribute to "
"your API key for the Case Access Project. See https://api.case.law/"
)
api_dict["Authorization"] = f"Token {self.api_token}"
return api_dict

def fetch_decision_list_by_cite(
self, cite: Union[str, CaseCitation, CAPCitation], full_case: bool = False
) -> List[RawDecision]:
"""
Get the "results" list for a queried citation from the CAP API.
:param cite:
a citation linked to an opinion in the
`Caselaw Access Project database <https://case.law/api/>`_.
Usually these will be in the traditional format
``[Volume Number] [Reporter Name Abbreviation] [Page Number]``, e.g.
`750 F.3d 1339 <https://case.law/search/#/cases?page=1&cite=%22750%20F.3d%201339%22>`_
for Oracle America, Inc. v. Google Inc.
:param full_case:
whether to request the full text of the opinion from the
`Caselaw Access Project API <https://api.case.law/v1/cases/>`_.
If this is ``True``, the CAPClient must have the `api_token` attribute.
:returns:
the "results" list for this queried citation.
"""

normalized_cite = normalize_case_cite(cite)

params = {"cite": normalized_cite}

headers = self.get_api_headers(full_case=full_case)

if full_case:
params["full_case"] = "true"
response = requests.get(self.endpoint, params=params, headers=headers).json()
return response["results"]

def fetch_by_cite(
self, cite: Union[str, CaseCitation, CAPCitation], full_case: bool = False
) -> RawDecision:
"""
Download a decision from Caselaw Access Project API.
:param cite:
a citation linked to an opinion in the
`Caselaw Access Project database <https://case.law/api/>`_.
Usually these will be in the traditional format
``[Volume Number] [Reporter Name Abbreviation] [Page Number]``, e.g.
`750 F.3d 1339 <https://case.law/search/#/cases?page=1&cite=%22750%20F.3d%201339%22>`_
for Oracle America, Inc. v. Google Inc.
:param full_case:
whether to request the full text of the opinion from the
`Caselaw Access Project API <https://api.case.law/v1/cases/>`_.
If this is ``True``, the CAPClient must have the `api_token` attribute.
:returns:
the first case in the "results" list for this queried citation.
"""
result_list = self.fetch_decision_list_by_cite(cite=cite, full_case=full_case)
return result_list[0]

def fetch_by_id(self, cap_id: int, full_case: bool = False) -> RawDecision:
"""
Download a decision from Caselaw Access Project API.
:param cap_id:
an identifier for an opinion in the
`Caselaw Access Project database <https://case.law/api/>`_,
e.g. 4066790 for
`Oracle America, Inc. v. Google Inc. <https://api.case.law/v1/cases/4066790/>`_.
:param full_case:
whether to request the full text of the opinion from the
`Caselaw Access Project API <https://api.case.law/v1/cases/>`_.
If this is ``True``, the CAPClient must have the `api_token` attribute.
:returns:
the first case in the "results" list for this queried citation.
"""
url = self.endpoint + f"{cap_id}/"
headers = self.get_api_headers(full_case=full_case)
params = {}
if full_case:
params["full_case"] = "true"
response = requests.get(url, params=params, headers=headers).json()
if cap_id and response.get("detail") == "Not found.":
raise AuthoritySpokeAPIError(f"API returned no cases with id {cap_id}")
return response
38 changes: 25 additions & 13 deletions authorityspoke/io/schemas_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Should be suitable for generating an OpenAPI specification.
"""

from typing import Dict, List, NamedTuple, Optional, Sequence, Tuple, Type, Union
from typing import Dict, List, NamedTuple, Optional, Sequence, TypedDict, Type, Union

from marshmallow import Schema, fields, EXCLUDE
from marshmallow import pre_load, post_load
Expand All @@ -17,7 +17,7 @@
from legislice.schemas import EnactmentSchema
from nettlesome.schemas import PredicateSchema, EntitySchema, RawFactor

from authorityspoke.decisions import CaseCitation, Decision
from authorityspoke.decisions import CAPCitation, CAPCitationTo, Decision
from authorityspoke.evidence import Exhibit, Evidence
from nettlesome.factors import Factor
from authorityspoke.facts import Fact
Expand All @@ -35,25 +35,36 @@


RawOpinion = Dict[str, str]
RawCaseCitation = Dict[str, str]
RawDecision = Dict[
str, Union[str, int, Sequence[RawOpinion], Sequence[RawCaseCitation]]
]
RawCAPCitation = Dict[str, str]
RawDecision = Dict[str, Union[str, int, Sequence[RawOpinion], Sequence[RawCAPCitation]]]


class CaseCitationSchema(Schema):
class CAPCitationSchema(Schema):
"""Schema for Decision citations in CAP API response."""

__model__ = CaseCitation
__model__ = CAPCitation
cite = fields.Str()
reporter = fields.Str(data_key="type")

@post_load
def make_object(self, data: RawCaseCitation, **kwargs) -> CaseCitation:
def make_object(self, data: RawCAPCitation, **kwargs) -> CAPCitation:
"""Load citation."""
return self.__model__(**data)


class CAPCitationToSchema(Schema):
"""Schema for Decision citations in CAP API response."""

__model__ = CAPCitationTo
cite = fields.Str()
case_ids = fields.List(fields.Int())

@post_load
def make_object(self, data: RawCAPCitation, **kwargs) -> CAPCitation:
"""Load citation."""
return self.__model__(cite=data["cite"])


class OpinionSchema(Schema):
"""Schema for Opinions, of which there may be several in one Decision."""

Expand All @@ -69,7 +80,7 @@ def format_data_to_load(self, data: RawOpinion, **kwargs) -> RawOpinion:
return data

@post_load
def make_object(self, data: RawOpinion, **kwargs) -> CaseCitation:
def make_object(self, data: RawOpinion, **kwargs) -> CAPCitation:
return self.__model__(**data)


Expand All @@ -79,7 +90,7 @@ class DecisionSchema(Schema):
__model__ = Decision
name = fields.Str()
name_abbreviation = fields.Str(missing=None)
citations = fields.Nested(CaseCitationSchema, many=True)
citations = fields.Nested(CAPCitationSchema, many=True)
opinions = fields.Nested(OpinionSchema, many=True)
first_page = fields.Int()
last_page = fields.Int()
Expand All @@ -90,7 +101,7 @@ class DecisionSchema(Schema):
# reporter = fields.Str(missing=None)
# volume = fields.Str(missing=None)
id = fields.Int()
cites_to = fields.Nested(CaseCitationSchema, many=True, missing=list)
cites_to = fields.Nested(CAPCitationToSchema, many=True, missing=list)

class Meta:
unknown = EXCLUDE
Expand All @@ -100,7 +111,8 @@ def format_data_to_load(self, data: RawDecision, **kwargs) -> RawDecision:
"""Transform data from CAP API response for loading."""
if not isinstance(data["court"], str):
data["court"] = data.get("court", {}).get("slug", "")
data["jurisdiction"] = data.get("jurisdiction", {}).get("slug", "")
if not isinstance(data["jurisdiction"], str):
data["jurisdiction"] = data.get("jurisdiction", {}).get("slug", "")
data["opinions"] = (
data.get("casebody", {}).get("data", {}).get("opinions", [{}])
)
Expand Down

0 comments on commit 48f45ba

Please sign in to comment.