Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .github/workflows/beancount.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
name: beancount

on:
[push, pull_request]
push:
branches: ['main', 'master']
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v4
with:
python-version: '3.9'
- run: pip install -r requirements_dev.txt
- run: pylint beanprice
- run: pytest beanprice
- run: mypy beanprice
python-version: ${{ matrix.python-version }}
- run: uv sync --group dev
- run: uv run ruff check beanprice
- run: uv run pylint beanprice
- run: uv run pytest beanprice
- run: uv run mypy beanprice
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ Otherwise read the source.
To install beanprice, run:

```shell
# using pip
pip install git+https://github.com/beancount/beanprice.git

# using uv
uv add git+https://github.com/beancount/beanprice.git
```

You can fetch the latest price of a stock by running:
Expand Down
26 changes: 13 additions & 13 deletions beanprice/price.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import sys
import logging
from concurrent import futures
from typing import Any, Dict, List, Optional, NamedTuple, Tuple
from typing import Any, Optional, NamedTuple
import diskcache

from dateutil import tz
Expand Down Expand Up @@ -60,7 +60,7 @@ class DatedPrice(NamedTuple):
base: Optional[str]
quote: Optional[str]
date: Optional[datetime.date]
sources: List[PriceSource]
sources: list[PriceSource]


# The Python package where the default sources are found.
Expand Down Expand Up @@ -102,7 +102,7 @@ def format_dated_price_str(dprice: DatedPrice) -> str:
)


def parse_source_map(source_map_spec: str) -> Dict[str, List[PriceSource]]:
def parse_source_map(source_map_spec: str) -> dict[str, list[PriceSource]]:
"""Parse a source map specification string.

Source map specifications allow the specification of multiple sources for
Expand Down Expand Up @@ -134,7 +134,7 @@ def parse_source_map(source_map_spec: str) -> Dict[str, List[PriceSource]]:
Raises:
ValueError: If an invalid pattern has been specified.
"""
source_map: Dict[str, List[PriceSource]] = collections.defaultdict(list)
source_map: dict[str, list[PriceSource]] = collections.defaultdict(list)
for source_list_spec in re.split("[ ;]", source_map_spec):
match = re.match("({}):(.*)$".format(amount.CURRENCY_RE), source_list_spec)
if not match:
Expand Down Expand Up @@ -202,7 +202,7 @@ def import_source(module_name: str):
def find_currencies_declared(
entries: data.Entries,
date: Optional[datetime.date] = None,
) -> List[Tuple[str, str, List[PriceSource]]]:
) -> list[tuple[str, str, list[PriceSource]]]:
"""Return currencies declared in Commodity directives.

If a 'price' metadata field is provided, include all the quote currencies
Expand Down Expand Up @@ -639,8 +639,8 @@ def fetch_price(dprice: DatedPrice, swap_inverted: bool = False) -> Optional[dat


def filter_redundant_prices(
price_entries: List[data.Price], existing_entries: List[data.Price], diffs: bool = False
) -> Tuple[List[data.Price], List[data.Price]]:
price_entries: list[data.Price], existing_entries: list[data.Price], diffs: bool = False
) -> tuple[list[data.Price], list[data.Price]]:
"""Filter out new entries that are redundant from an existing set.

If the price differs, we override it with the new entry only on demand. This
Expand All @@ -663,8 +663,8 @@ def filter_redundant_prices(
for entry in existing_entries
if isinstance(entry, data.Price)
}
filtered_prices: List[data.Price] = []
ignored_prices: List[data.Price] = []
filtered_prices: list[data.Price] = []
ignored_prices: list[data.Price] = []
for entry in price_entries:
key = (entry.date, entry.currency)
if key in existing_prices:
Expand All @@ -680,9 +680,9 @@ def filter_redundant_prices(
return filtered_prices, ignored_prices


def process_args() -> Tuple[
def process_args() -> tuple[
argparse.Namespace,
List[DatedPrice],
list[DatedPrice],
data.Directives,
Optional[Any],
]:
Expand Down Expand Up @@ -887,7 +887,7 @@ def process_args() -> Tuple[
if args.expressions:
# Interpret the arguments as price sources.
for source_str in args.sources:
psources: List[PriceSource] = []
psources: list[PriceSource] = []
try:
psource_map = parse_source_map(source_str)
except ValueError:
Expand Down Expand Up @@ -940,7 +940,7 @@ def process_args() -> Tuple[
)
continue
logging.info('Loading "%s"', filename)
entries, errors, options_map = loader.load_file(filename, log_errors=sys.stderr)
entries, _errors, options_map = loader.load_file(filename, log_errors=sys.stderr)
if dcontext is None:
dcontext = options_map["dcontext"]
for date in dates:
Expand Down
6 changes: 3 additions & 3 deletions beanprice/price_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,20 +209,20 @@ def test_explicit_file__badcontents(self, filename):
2015-01-01 open USD ;; Error
"""
with test_utils.capture("stderr"):
args, jobs, _, __ = run_with_args(price.process_args, ["--no-cache", filename])
_args, jobs, _, __ = run_with_args(price.process_args, ["--no-cache", filename])
self.assertEqual([], jobs)

def test_filename_exists(self):
with tempfile.NamedTemporaryFile("w") as tmpfile:
with test_utils.capture("stderr"):
args, jobs, _, __ = run_with_args(
_args, jobs, _, __ = run_with_args(
price.process_args, ["--no-cache", tmpfile.name]
)
self.assertEqual([], jobs) # Empty file.

def test_expressions(self):
with test_utils.capture("stderr"):
args, jobs, _, __ = run_with_args(
_args, jobs, _, __ = run_with_args(
price.process_args, ["--no-cache", "-e", "USD:yahoo/AAPL"]
)
self.assertEqual(
Expand Down
4 changes: 2 additions & 2 deletions beanprice/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import datetime
from decimal import Decimal
from typing import List, Optional, NamedTuple
from typing import Optional, NamedTuple


# A record that contains data for a price fetched from a source.
Expand Down Expand Up @@ -93,7 +93,7 @@ def get_historical_price(

def get_prices_series(
self, ticker: str, time_begin: datetime.datetime, time_end: datetime.datetime
) -> Optional[List[SourcePrice]]:
) -> Optional[list[SourcePrice]]:
"""Return the historical daily price series between two dates.

Note that weekends don't have any prices, so there's no guarantee that
Expand Down
8 changes: 4 additions & 4 deletions beanprice/sources/coincap.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from datetime import datetime, timezone, timedelta
import math
from decimal import Decimal
from typing import List, Optional, Dict
from typing import Optional
import requests
from beanprice import source

Expand All @@ -26,7 +26,7 @@ class CoincapError(ValueError):
"An error from the Coincap importer."


def get_asset_list() -> List[Dict[str, str]]:
def get_asset_list() -> list[dict[str, str]]:
"""
Get list of currencies supported by Coincap. Returned is a list with
elements with many properties, including "id", representing the Coincap id,
Expand Down Expand Up @@ -85,7 +85,7 @@ def get_latest_price(base_currency: str) -> source.SourcePrice:

def get_price_series(
base_currency_id: str, time_begin: datetime, time_end: datetime
) -> List[source.SourcePrice]:
) -> list[source.SourcePrice]:
path = f"assets/{base_currency_id}/history"
params = {
"interval": "d1",
Expand Down Expand Up @@ -129,5 +129,5 @@ def get_historical_price(

def get_prices_series(
self, ticker: str, time_begin: datetime, time_end: datetime
) -> List[source.SourcePrice]:
) -> list[source.SourcePrice]:
return get_price_series(resolve_currency_id(ticker), time_begin, time_end)
6 changes: 3 additions & 3 deletions beanprice/sources/ecbrates_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ def response(contents, status_code=requests.codes.ok):

class ECBRatesErrorFetcher(unittest.TestCase):
def test_error_invalid_ticker(self):
with self.assertRaises(ValueError) as exc:
with self.assertRaises(ValueError):
ecbrates.Source().get_latest_price("INVALID")

def test_error_network(self):
with response("Foobar", 404):
with self.assertRaises(ValueError) as exc:
with self.assertRaises(ValueError):
ecbrates.Source().get_latest_price("EUR-SEK")

def test_empty_response(self):
with response("", 200):
with self.assertRaises(ecbrates.ECBRatesError) as exc:
with self.assertRaises(ecbrates.ECBRatesError):
ecbrates.Source().get_latest_price("EUR-SEK")

def test_valid_response(self):
Expand Down
12 changes: 6 additions & 6 deletions beanprice/sources/yahoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Optional, Union

from curl_cffi import requests

Expand All @@ -31,7 +31,7 @@ class YahooError(ValueError):
"An error from the Yahoo API."


def parse_response(response: requests.models.Response) -> Dict:
def parse_response(response: requests.models.Response) -> dict:
"""Process as response from Yahoo.

Raises:
Expand Down Expand Up @@ -62,7 +62,7 @@ def parse_response(response: requests.models.Response) -> Dict:
}


def parse_currency(result: Dict[str, Any]) -> Optional[str]:
def parse_currency(result: dict[str, Any]) -> Optional[str]:
"""Infer the currency from the result."""
if "market" not in result:
return None
Expand All @@ -81,13 +81,13 @@ def get_price_series(
time_begin: datetime,
time_end: datetime,
session: requests.Session,
) -> Tuple[List[Tuple[datetime, Decimal]], str]:
) -> tuple[list[tuple[datetime, Decimal]], str]:
"""Return a series of timestamped prices."""

if requests is None:
raise YahooError("You must install the 'requests' library.")
url = "https://query1.finance.yahoo.com/v8/finance/chart/{}".format(ticker)
payload: Dict[str, Union[int, str]] = {
payload: dict[str, Union[int, str]] = {
"period1": int(time_begin.timestamp()),
"period2": int(time_end.timestamp()),
"interval": "1d",
Expand Down Expand Up @@ -203,7 +203,7 @@ def get_historical_price(

def get_daily_prices(
self, ticker: str, time_begin: datetime, time_end: datetime
) -> Optional[List[source.SourcePrice]]:
) -> Optional[list[source.SourcePrice]]:
"""See contract in beanprice.source.Source."""
series, currency = get_price_series(ticker, time_begin, time_end, self.session)
return [source.SourcePrice(price, time, currency) for time, price in series]
3 changes: 1 addition & 2 deletions experiments/dividends/download_dividends.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from datetime import date as Date
from decimal import Decimal
from typing import List, Tuple
import argparse
import csv
import datetime
Expand All @@ -19,7 +18,7 @@

def download_dividends(
instrument: str, start_date: Date, end_date: Date
) -> List[Tuple[Date, Decimal]]:
) -> list[tuple[Date, Decimal]]:
"""Download a list of dividends issued over a time interval."""
tim = datetime.time()
payload = {
Expand Down
34 changes: 5 additions & 29 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ['setuptools']
requires = ['setuptools', 'uv']
build-backend = 'setuptools.build_meta'

[project]
Expand Down Expand Up @@ -48,7 +48,8 @@ issues = 'https://github.com/beancount/beanprice/issues'
dev = [
"pytest>=8.3.5",
"pylint==3.3.3",
"mypy==1.14.1"
"mypy==1.14.1",
"ruff>=0.9.0"
]

[tool.setuptools.packages]
Expand All @@ -62,34 +63,9 @@ exclude_also = [
'if typing.TYPE_CHECKING:',
]

[tool.ruff]
line-length = 92
target-version = 'py39'

[tool.ruff.lint]
select = ['E', 'F', 'W', 'UP', 'B', 'C4', 'PL', 'RUF']

# TODO(blais): Review these ignores.
# pylint and ruff are not aligned on this... maybe removing pylint would solve the problem.
ignore = [
'RUF013',
'RUF005',
'PLW0603',
'UP014',
'UP031',
'B007',
'B905',
'C408',
'E731',
'PLR0911',
'PLR0912',
'PLR0913',
'PLR0915',
'PLR1714',
'PLR2004',
'PLW2901',
'RUF012',
'UP007',
'UP032',
'C0301',
]

[tool.mypy]
Expand Down
Loading