Skip to content

Commit

Permalink
Merge pull request #113 from mscarey/replace-binary-cassettes
Browse files Browse the repository at this point in the history
Replace binary cassettes
  • Loading branch information
mscarey authored Jan 21, 2025
2 parents c460ae4 + 1c1ffe8 commit 6d17e76
Show file tree
Hide file tree
Showing 32 changed files with 7,983 additions and 1,897 deletions.
13 changes: 4 additions & 9 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,21 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.11", "3.12"]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-recording coveralls==2.1.2 pytest-cov
pip install pytest pytest-recording pytest-cov
- name: Test with pytest
env:
CAP_API_KEY: ${{ secrets.CAP_API_KEY }}
run: |
pytest tests/ --record-mode=none --cov=authorityspoke --cov-report=term-missing
- name: Upload coverage data to coveralls.io
if: matrix.python-version == 3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: coveralls
1 change: 1 addition & 0 deletions authorityspoke/decisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class DecisionReading(BaseModel, Comparable):
"""An interpretation of what Holdings are supported by the Opinions of a Decision."""

decision: Decision
generic: bool = False
opinion_readings: List[OpinionReading] = []

def __str__(self):
Expand Down
76 changes: 40 additions & 36 deletions authorityspoke/facts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Create models of assertions accepted as factual by courts."""

from __future__ import annotations
from copy import deepcopy
import operator
Expand Down Expand Up @@ -98,32 +99,35 @@ class Config:
@root_validator(pre=True)
def nest_predicate_fields(cls, values):
"""Move fields passed to the Fact model that really belong to the Predicate model."""
type_str = values.pop("type", "")
if type_str and type_str.lower() != "fact":
raise ValidationError(f"type {type_str} was passed to Fact model")

if isinstance(values.get("predicate"), str):
values["predicate"] = Predicate(content=values["predicate"])
if "truth" in values:
values["predicate"].truth = values.pop("truth")

for field_name in ["content", "truth", "sign", "expression"]:
if field_name in values:
values["predicate"] = values.get("predicate", {})
values["predicate"][field_name] = values.pop(field_name)
if isinstance(values.get("predicate"), dict) and values["predicate"].get(
"content"
):
for sign in {
**QuantityRange.opposite_comparisons,
**QuantityRange.normalized_comparisons,
}:
if sign in values["predicate"]["content"]:
content, quantity_text = values["predicate"]["content"].split(sign)
values["predicate"]["content"] = content.strip()
values["predicate"]["expression"] = quantity_text.strip()
values["predicate"]["sign"] = sign
break
if isinstance(values, dict):
type_str = values.pop("type", "")
if type_str and type_str.lower() != "fact":
raise ValueError(f"type {type_str} was passed to Fact model")

if isinstance(values.get("predicate"), str):
values["predicate"] = Predicate(content=values["predicate"])
if "truth" in values:
values["predicate"].truth = values.pop("truth")

for field_name in ["content", "truth", "sign", "expression"]:
if field_name in values:
values["predicate"] = values.get("predicate", {})
values["predicate"][field_name] = values.pop(field_name)
if isinstance(values.get("predicate"), dict) and values["predicate"].get(
"content"
):
for sign in {
**QuantityRange.opposite_comparisons,
**QuantityRange.normalized_comparisons,
}:
if sign in values["predicate"]["content"]:
content, quantity_text = values["predicate"]["content"].split(
sign
)
values["predicate"]["content"] = content.strip()
values["predicate"]["expression"] = quantity_text.strip()
values["predicate"]["sign"] = sign
break
return values

@validator("terms", pre=True)
Expand Down Expand Up @@ -190,7 +194,7 @@ def _validate_terms(cls, v, values, **kwargs):
TermSequence.validate_terms(v)

if values.get("predicate") is None:
raise ValidationError("Predicate field is required.")
raise ValueError("Predicate field is required.")

if len(v) != len(values["predicate"]):
message = (
Expand Down Expand Up @@ -475,7 +479,7 @@ class Exhibit(Factor, BaseModel):

def _means_if_concrete(
self, other: Factor, context: ContextRegister
) -> Iterator[ContextRegister]:
) -> Iterator[Explanation]:
if (
isinstance(other, self.__class__)
and self.form == other.form
Expand All @@ -485,15 +489,15 @@ def _means_if_concrete(

def _implies_if_concrete(
self, other: Factor, context: Optional[ContextRegister] = None
) -> Iterator[ContextRegister]:
) -> Iterator[Explanation]:
if isinstance(other, self.__class__) and (
self.form == other.form or other.form is None
):
yield from super()._implies_if_concrete(other, context)

def __str__(self):
"""Represent object as string without line breaks."""
string = f'{("attributed to " + self.statement_attribution.short_string) if self.statement_attribution else ""}'
string = f"{('attributed to ' + self.statement_attribution.short_string) if self.statement_attribution else ''}"
if self.statement:
string += ", asserting " + self.statement.short_string + ","
string = super().__str__().format(string)
Expand Down Expand Up @@ -561,13 +565,13 @@ def check_type_field(cls, values):
"""Fail valitation if the input has a "type" field without the class name."""
type_str = values.pop("type", "")
if type_str and type_str.lower() != "evidence":
raise ValidationError(f"type {type_str} was passed to Evidence model")
raise ValueError(f"type {type_str} was passed to Evidence model")
return values

def __str__(self):
string = (
f'{("of " + self.exhibit.short_string + " ") if self.exhibit else ""}'
+ f'{("which supports " + self.to_effect.short_string) if self.to_effect else ""}'
f"{('of ' + self.exhibit.short_string + ' ') if self.exhibit else ''}"
+ f"{('which supports ' + self.to_effect.short_string) if self.to_effect else ''}"
)
return super().__str__().format(string).strip().replace("Evidence", "evidence")

Expand Down Expand Up @@ -608,7 +612,7 @@ class Pleading(Factor, BaseModel):
context_factor_names: ClassVar[Tuple[str]] = ("filer",)

def __str__(self):
string = f'{("filed by " + self.filer.short_string if self.filer else "")}'
string = f"{('filed by ' + self.filer.short_string if self.filer else '')}"
return super().__str__().format(string)


Expand Down Expand Up @@ -655,8 +659,8 @@ def wrapped_string(self):

def __str__(self):
string = (
f'{("in " + self.pleading.short_string + ",") if self.pleading else ""}'
+ f'{("claiming " + self.fact.short_string + ",") if self.fact else ""}'
f"{('in ' + self.pleading.short_string + ',') if self.pleading else ''}"
+ f"{('claiming ' + self.fact.short_string + ',') if self.fact else ''}"
)
string = string.strip(",")
return super().__str__().format(string).replace("Allegation", "allegation")
Expand Down
3 changes: 1 addition & 2 deletions authorityspoke/holdings.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ def _contradicts_if_not_exclusive(
def _explanations_implies_if_not_exclusive(
self, other: Factor, context: Explanation
) -> Iterator[Explanation]:

if self.decided and other.decided:
yield from self._implies_if_decided(other, context)

Expand Down Expand Up @@ -408,7 +407,7 @@ def explanations_implication(
self,
other: Comparable,
context: Optional[Union[ContextRegister, Explanation]] = None,
) -> Iterator[ContextRegister]:
) -> Iterator[Explanation]:
"""Yield contexts that would cause self and other to have same meaning."""
if not self.comparable_with(other):
raise TypeError(f"Type Holding cannot be compared with type {type(other)}.")
Expand Down
21 changes: 12 additions & 9 deletions authorityspoke/io/name_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ def assign_name_for_evidence(obj: Dict) -> str:
"""
name = "evidence"
if obj.get("exhibit"):
name += f' of {obj["exhibit"]}'
name += f" of {obj['exhibit']}"
if obj.get("to_effect"):
name += f' to the effect that {obj["to_effect"]}'
name += f" to the effect that {obj['to_effect']}"
return name


Expand All @@ -120,7 +120,7 @@ def assign_name_for_pleading(obj: Dict) -> str:
"""
name = "pleading"
if obj.get("filer"):
name += f' filed by {obj["filer"]}'
name += f" filed by {obj['filer']}"
return name


Expand All @@ -130,7 +130,7 @@ def create_name_for_enactment(obj: RawEnactment) -> str:
return create_name_for_enactment(obj["enactment"])
name: str = obj["node"]
if obj.get("start_date"):
name += f'@{obj["start_date"]}'
name += f"@{obj['start_date']}"

for field_name in ["start", "end", "prefix", "exact", "suffix"]:
if obj.get(field_name):
Expand Down Expand Up @@ -284,7 +284,10 @@ def update_name_index_from_fact_content(

for name in mentioned.keys():
if name in content and name != content:
(content, terms,) = text_expansion.add_found_context(
(
content,
terms,
) = text_expansion.add_found_context(
content=content,
terms=terms,
factor=mentioned.get_by_name(name),
Expand Down Expand Up @@ -375,10 +378,10 @@ def collect_mentioned(
if new_dict.get("predicate", {}).get("content"):
for factor in new_dict.get("terms", []):
if factor not in new_dict["predicate"]["content"]:
new_dict["predicate"][
"content"
] = text_expansion.replace_brackets_with_placeholder(
content=new_dict["predicate"]["content"], name=factor
new_dict["predicate"]["content"] = (
text_expansion.replace_brackets_with_placeholder(
content=new_dict["predicate"]["content"], name=factor
)
)

new_dict, mentioned = update_name_index_with_factor(new_dict, mentioned)
Expand Down
3 changes: 2 additions & 1 deletion authorityspoke/io/writers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Functions for saving objects to file after they have been JSON serialized."""

import json
import pathlib

Expand Down Expand Up @@ -40,4 +41,4 @@ def case_to_file(
filename, directory, filepath, default_folder="cases"
)
with open(validated_filepath, "w") as fp:
fp.write(case.json(indent=4))
fp.write(case.model_dump_json(indent=4))
3 changes: 2 additions & 1 deletion authorityspoke/opinions.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class OpinionReading(Comparable, BaseModel):
"""An interpretation of what Holdings are supported by the text of an Opinion."""

anchored_holdings: AnchoredHoldings = AnchoredHoldings()
generic: bool = False
opinion_type: str = ""
opinion_author: str = ""

Expand Down Expand Up @@ -417,7 +418,7 @@ def posit_holdings(
)
else:
holding_anchors = holding_anchors or []
for (holding, selector_list) in zip_longest(holdings, holding_anchors):
for holding, selector_list in zip_longest(holdings, holding_anchors):
self.posit_holding(
holding=holding,
holding_anchors=selector_list,
Expand Down
5 changes: 3 additions & 2 deletions authorityspoke/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ def validate_enactment_groups(
"""Convert EnactmentPassage to EnactmentGroup."""
if isinstance(v, EnactmentPassage):
v = {"passages": [v]}
elif v and not isinstance(v, EnactmentGroup):
elif not v:
return EnactmentGroup()
elif not isinstance(v, EnactmentGroup):
if not isinstance(v, dict) or "passages" not in v:
try:
v = EnactmentGroup(passages=list(v)) if v else EnactmentGroup()
Expand Down Expand Up @@ -453,7 +455,6 @@ def explanations_implication(
and self.mandatory >= other.mandatory
and self.universal >= other.universal
):

if self.universal > other.universal:
yield from self.procedure.explain_implication_all_to_some(
other.procedure, context
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/create_holding_data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ into :class:`authorityspoke.decisions.Decision` objects.
>>> from authorityspoke import Decision, DecisionReading
>>> from authorityspoke.io.loaders import load_decision_as_reading
>>> from authorityspoke.io import CAPClient
>>> load_dotenv()
>>> load_dotenv(dotenv_path=".env")
True
>>> if USE_REAL_CASE_API:
... CAP_API_KEY = os.getenv('CAP_API_KEY')
Expand Down Expand Up @@ -123,7 +123,7 @@ the Holdings to the :class:`~authorityspoke.opinions.OpinionReading` using
the :meth:`~authorityspoke.opinions.OpinionReading.posit` method. As we look at
the parts of the JSON file, the code cells will show how fields from the
JSON affect the structure of the :class:`~authorityspoke.holdings.Holding`.

>>> oracle.posit(oracle_holdings)
>>> lotus.posit(lotus_holdings)
>>> print(oracle.holdings[0])
Expand Down Expand Up @@ -163,7 +163,7 @@ becomes one of the input’s ``terms``. If such an object hasn’t
been referenced before in the file, it will be created.

>>> print(oracle.holdings[0].inputs[0].terms)
[Entity(name='the Java API', generic=True, plural=False)]
[Entity(generic=True, absent=False, name='the Java API', plural=False)]


The JSON representation of a Rule can also have “mandatory” and
Expand Down Expand Up @@ -251,7 +251,7 @@ shows how to generate the schema as a Python dict and then view just the
>>> from authorityspoke.holdings import Holding
>>> schema = Holding.schema()
>>> schema["properties"]
{'rule': {'$ref': '#/definitions/Rule'}, 'rule_valid': {'title': 'Rule Valid', 'default': True, 'type': 'boolean'}, 'decided': {'title': 'Decided', 'default': True, 'type': 'boolean'}, 'exclusive': {'title': 'Exclusive', 'default': False, 'type': 'boolean'}, 'generic': {'title': 'Generic', 'default': False, 'type': 'boolean'}, 'absent': {'title': 'Absent', 'default': False, 'type': 'boolean'}}
{'generic': {'default': False, 'title': 'Generic', 'type': 'boolean'}, 'absent': {'default': False, 'title': 'Absent', 'type': 'boolean'}, 'rule': {'$ref': '#/$defs/Rule'}, 'rule_valid': {'default': True, 'title': 'Rule Valid', 'type': 'boolean'}, 'decided': {'default': True, 'title': 'Decided', 'type': 'boolean'}, 'exclusive': {'default': False, 'title': 'Exclusive', 'type': 'boolean'}}

The schema can also be exported as JSON using
the :meth:`authorityspoke.holdings.Holding.schema_json` method.
12 changes: 7 additions & 5 deletions docs/guides/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ make it visible on the internet.

>>> import os
>>> from dotenv import load_dotenv
>>> load_dotenv()
>>> load_dotenv(".env")
True
>>> CAP_API_KEY = os.getenv('CAP_API_KEY')

Expand Down Expand Up @@ -337,7 +337,10 @@ using the ``.dict()`` or ``.json()`` methods.
'name': 'false the Java API was copyrightable',
'predicate': {'content': '${the_java_api} was copyrightable', 'truth': False},
'standard_of_proof': None,
'terms': [{'generic': True, 'name': 'the Java API', 'plural': False}]}]
'terms': [{'absent': False,
'generic': True,
'name': 'the Java API',
'plural': False}]}]

Linking Holdings to Opinions
-------------------------------
Expand Down Expand Up @@ -443,7 +446,7 @@ indicate that the Java API is a generic :class:`nettlesome.entities.Entity` ment
in the :class:`~authorityspoke.facts.Fact`\.

>>> oracle.holdings[0].generic_terms()
[Entity(name='the Java API', generic=True, plural=False)]
[Entity(generic=True, absent=False, name='the Java API', plural=False)]

A generic :class:`~nettlesome.entities.Entity` is “generic”
in the sense that in the context of
Expand Down Expand Up @@ -482,8 +485,7 @@ angle brackets in the string representation of
the :class:`~authorityspoke.holdings.Holding`\.

>>> lotus.holdings[0].generic_terms()
[Entity(name='Borland International', generic=True, plural=False), Entity(name='the Lotus menu command hierarchy', generic=True, plural=False)]

[Entity(generic=True, absent=False, name='Borland International', plural=False), Entity(generic=True, absent=False, name='the Lotus menu command hierarchy', plural=False)]

The same :class:`~authorityspoke.rules.Rule`\s and
:class:`~authorityspoke.holdings.Holding`\s may be relevant to more than one
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/load_yaml_holdings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ documentation <https://saurabh-kumar.com/python-dotenv/#getting-started>`__.
>>> from datetime import date
>>> import os
>>> from dotenv import load_dotenv
>>> load_dotenv()
>>> load_dotenv(".env")
True
>>> CAP_API_KEY = os.getenv('CAP_API_KEY')
>>> USE_REAL_CASE_API = False
Expand Down
Loading

0 comments on commit 6d17e76

Please sign in to comment.