Skip to content

Commit bb50c71

Browse files
the-maceclaude
andcommitted
Fix CI/CD pipeline failures
This commit fixes multiple issues that caused the GitHub Actions CI pipeline to fail: **Linting Fixes:** - Fixed line length violations in kubera/formatters.py (E501) - Removed unnecessary mode argument in kubera/cache.py (UP015) - Replaced IOError with OSError in kubera/cache.py (UP024) - Removed unused import in kubera/cli.py (F401) - Fixed f-string without placeholders in kubera/cli.py (F541) - Fixed variable naming in tests/test_cli.py (N806) - Removed unused variable in tests/test_cli.py (F841) - Formatted all test files with ruff format **Type Checking Fixes:** - Added proper type annotations for portfolio items in CLI - Fixed mypy type errors in kubera/cli.py **Test Configuration Fixes:** - Removed --asyncio-mode=auto from pytest.ini to fix pytest-asyncio compatibility - This resolves AttributeError: 'Package' object has no attribute 'obj' **Code Quality:** - All tests pass (58/58) - All mypy type checks pass - All ruff lint checks pass - Updated CLAUDE.md with development guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cf14264 commit bb50c71

12 files changed

Lines changed: 241 additions & 275 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,5 @@ All endpoints use `https://api.kubera.com` as base URL:
208208
- Line length: 100 characters
209209
- Use Ruff for formatting and linting
210210
- Docstrings required for all public methods
211+
- Always run the lint and unit tests before pushing any code. Squash merges on branches too.
212+
- Always include claude.md changes with commits when it has been updated

examples/async_usage.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ async def main() -> None:
2020

2121
for portfolio in portfolio_data:
2222
net_worth = portfolio.get("net_worth", {})
23-
print(
24-
f"{portfolio['name']}: {net_worth.get('amount', 0)} {portfolio['currency']}"
25-
)
23+
print(f"{portfolio['name']}: {net_worth.get('amount', 0)} {portfolio['currency']}")
2624

2725
# Update an item asynchronously
2826
# item_id = "your_asset_or_debt_id"

kubera/auth.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ def generate_signature(
4040
data = f"{api_key}{timestamp}{http_method}{request_path}{body_data}"
4141

4242
# Generate HMAC-SHA256 signature
43-
signature = hmac.new(
44-
secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256
45-
).hexdigest()
43+
signature = hmac.new(secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).hexdigest()
4644

4745
return signature, timestamp
4846

kubera/cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ def load_portfolio_cache() -> list[dict[str, Any]]:
4848
return []
4949

5050
try:
51-
with open(cache_file, "r") as f:
51+
with open(cache_file) as f:
5252
cache_data = json.load(f)
5353
return cache_data.get("portfolios", [])
54-
except (json.JSONDecodeError, IOError):
54+
except (json.JSONDecodeError, OSError):
5555
return []
5656

5757

kubera/cli.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Command-line interface for Kubera API."""
22

33
import sys
4+
from typing import Any
45

56
import click
67

7-
from kubera.cache import load_portfolio_cache, resolve_portfolio_id, save_portfolio_cache
8+
from kubera.cache import resolve_portfolio_id, save_portfolio_cache
89
from kubera.client import KuberaClient
910
from kubera.exceptions import KuberaAPIError
1011
from kubera.formatters import (
@@ -90,7 +91,7 @@ def list(ctx: click.Context, raw: bool) -> None:
9091
try:
9192
portfolios = client.get_portfolios()
9293
# Save to cache for index-based lookups
93-
save_portfolio_cache(portfolios)
94+
save_portfolio_cache(portfolios) # type: ignore[arg-type]
9495
print_portfolios(portfolios, raw=raw) # type: ignore[arg-type]
9596
except KuberaAPIError as e:
9697
print_error(f"Failed to fetch portfolios: {e.message}")
@@ -153,9 +154,7 @@ def show(ctx: click.Context, portfolio_id: str, raw: bool, tree: bool) -> None:
153154
@click.argument("sheet_name")
154155
@click.option("--raw", is_flag=True, help="Output raw JSON instead of formatted text")
155156
@click.pass_context
156-
def drill(
157-
ctx: click.Context, portfolio_id: str, category: str, sheet_name: str, raw: bool
158-
) -> None:
157+
def drill(ctx: click.Context, portfolio_id: str, category: str, sheet_name: str, raw: bool) -> None:
159158
"""Drill down into a specific sheet within a category.
160159
161160
Shows detailed information for all items in a specific sheet, including:
@@ -189,6 +188,7 @@ def drill(
189188
portfolio = client.get_portfolio(resolved_id)
190189

191190
# Get the items from the specified category
191+
items: Any
192192
if category.lower() == "asset":
193193
items = portfolio.get("asset", portfolio.get("assets", []))
194194
elif category.lower() == "debt":
@@ -203,7 +203,8 @@ def drill(
203203
sheet_items = [
204204
item
205205
for item in items
206-
if item.get("sheetName", "").lower() == sheet_name.lower()
206+
if isinstance(item.get("sheetName"), str)
207+
and item.get("sheetName", "").lower() == sheet_name.lower()
207208
]
208209

209210
if not sheet_items:
@@ -312,7 +313,7 @@ def test(ctx: click.Context, raw: bool) -> None:
312313
portfolios = client.get_portfolios()
313314

314315
if not raw:
315-
print_success(f"✓ Successfully connected to Kubera API!")
316+
print_success("✓ Successfully connected to Kubera API!")
316317
print_success(f"✓ Found {len(portfolios)} portfolio(s)")
317318

318319
if portfolios:
@@ -418,7 +419,7 @@ def interactive(ctx: click.Context) -> None:
418419
return
419420

420421
# Save to cache and show portfolios
421-
save_portfolio_cache(portfolios)
422+
save_portfolio_cache(portfolios) # type: ignore[arg-type]
422423
print_portfolios(portfolios) # type: ignore[arg-type]
423424

424425
# Let user choose a portfolio

kubera/client.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,15 @@ def _handle_response(self, response: httpx.Response) -> Any:
188188
" Note: Update operations require an API key with update permissions enabled. "
189189
"Read-only API keys cannot modify data."
190190
)
191-
raise KuberaAPIError(
192-
f"Permission denied: {error_message}.{hint}", response.status_code
193-
)
191+
raise KuberaAPIError(f"Permission denied: {error_message}.{hint}", response.status_code)
194192
elif response.status_code == 429:
195193
raise KuberaRateLimitError(
196194
f"Rate limit exceeded: {error_message}. "
197195
"Limits: 30 req/min, 100/day (Essential) or 1000/day (Black)",
198196
response.status_code,
199197
)
200198
elif response.status_code == 400:
201-
raise KuberaValidationError(
202-
f"Validation error: {error_message}", response.status_code
203-
)
199+
raise KuberaValidationError(f"Validation error: {error_message}", response.status_code)
204200
else:
205201
raise KuberaAPIError(
206202
f"API error ({response.status_code}): {error_message}", response.status_code

kubera/formatters.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def format_number(value: float | int | str) -> str:
4141
return str(value)
4242
if isinstance(value, float):
4343
# Remove trailing zeros after decimal point
44-
formatted = f"{value:,.4f}".rstrip('0').rstrip('.')
44+
formatted = f"{value:,.4f}".rstrip("0").rstrip(".")
4545
return formatted
4646
return f"{value:,}"
4747

@@ -78,7 +78,9 @@ def print_portfolios(portfolios: list[dict[str, Any]], raw: bool = False) -> Non
7878
)
7979

8080
console.print(table)
81-
console.print("\n[dim]Tip: Use the index number (e.g., 'kubera show 1') instead of the full ID[/dim]")
81+
console.print(
82+
"\n[dim]Tip: Use the index number (e.g., 'kubera show 1') instead of the full ID[/dim]"
83+
)
8284

8385

8486
def print_portfolio(portfolio: dict[str, Any], raw: bool = False) -> None:
@@ -114,7 +116,9 @@ def print_portfolio(portfolio: dict[str, Any], raw: bool = False) -> None:
114116
assets = portfolio.get("asset", portfolio.get("assets", []))
115117
if assets:
116118
asset_total = portfolio.get("assetTotal")
117-
total_str = f" - Total: {format_currency(asset_total, 'USD')}" if asset_total is not None else ""
119+
total_str = (
120+
f" - Total: {format_currency(asset_total, 'USD')}" if asset_total is not None else ""
121+
)
118122
console.print(f"\n[bold]Assets ({len(assets)} items){total_str}[/bold]")
119123

120124
# Group by sheet
@@ -138,19 +142,17 @@ def print_portfolio(portfolio: dict[str, Any], raw: bool = False) -> None:
138142

139143
for sheet_name, (items, total) in by_sheet.items():
140144
currency = items[0].get("value", {}).get("currency", "USD") if items else "USD"
141-
sheet_table.add_row(
142-
sheet_name,
143-
str(len(items)),
144-
format_currency(total, currency)
145-
)
145+
sheet_table.add_row(sheet_name, str(len(items)), format_currency(total, currency))
146146

147147
console.print(sheet_table)
148148

149149
# Debts - API uses 'debt' not 'debts'
150150
debts = portfolio.get("debt", portfolio.get("debts", []))
151151
if debts:
152152
debt_total = portfolio.get("debtTotal")
153-
total_str = f" - Total: {format_currency(debt_total, 'USD')}" if debt_total is not None else ""
153+
total_str = (
154+
f" - Total: {format_currency(debt_total, 'USD')}" if debt_total is not None else ""
155+
)
154156
console.print(f"\n[bold]Debts ({len(debts)} items){total_str}[/bold]")
155157

156158
# Group by sheet
@@ -174,11 +176,7 @@ def print_portfolio(portfolio: dict[str, Any], raw: bool = False) -> None:
174176

175177
for sheet_name, (items, total) in by_sheet_debt.items():
176178
currency = items[0].get("value", {}).get("currency", "USD") if items else "USD"
177-
debt_sheet_table.add_row(
178-
sheet_name,
179-
str(len(items)),
180-
format_currency(total, currency)
181-
)
179+
debt_sheet_table.add_row(sheet_name, str(len(items)), format_currency(total, currency))
182180

183181
console.print(debt_sheet_table)
184182

@@ -212,9 +210,7 @@ def print_portfolio(portfolio: dict[str, Any], raw: bool = False) -> None:
212210
for sheet_name, (items, total) in by_sheet_ins.items():
213211
currency = items[0].get("value", {}).get("currency", "USD") if items else "USD"
214212
ins_sheet_table.add_row(
215-
sheet_name,
216-
str(len(items)),
217-
format_currency(total, currency)
213+
sheet_name, str(len(items)), format_currency(total, currency)
218214
)
219215

220216
console.print(ins_sheet_table)
@@ -357,7 +353,11 @@ def print_success(message: str) -> None:
357353

358354

359355
def print_sheet_detail(
360-
items: list[dict[str, Any]], sheet_name: str, category: str, portfolio_name: str, raw: bool = False
356+
items: list[dict[str, Any]],
357+
sheet_name: str,
358+
category: str,
359+
portfolio_name: str,
360+
raw: bool = False,
361361
) -> None:
362362
"""Print detailed information for items in a sheet.
363363
@@ -386,7 +386,9 @@ def print_sheet_detail(
386386

387387
# Calculate totals (excluding parent accounts)
388388
total_value = sum(item.get("value", {}).get("amount", 0) or 0 for item in items_for_totals)
389-
total_cost = sum(item.get("cost", {}).get("amount", 0) or 0 for item in items_for_totals if "cost" in item)
389+
total_cost = sum(
390+
item.get("cost", {}).get("amount", 0) or 0 for item in items_for_totals if "cost" in item
391+
)
390392

391393
# Overall gains
392394
if total_cost > 0:
@@ -397,7 +399,8 @@ def print_sheet_detail(
397399
console.print(
398400
f"\n[bold]Total Value:[/bold] {format_currency(total_value, 'USD')} | "
399401
f"[bold]Cost Basis:[/bold] {format_currency(total_cost, 'USD')} | "
400-
f"[bold {gain_color}]Gain:[/bold {gain_color}] {gain_sign}{format_currency(total_gain, 'USD')} "
402+
f"[bold {gain_color}]Gain:[/bold {gain_color}] "
403+
f"{gain_sign}{format_currency(total_gain, 'USD')} "
401404
f"({gain_sign}{total_gain_pct:.2f}%)"
402405
)
403406
else:
@@ -412,13 +415,17 @@ def print_sheet_detail(
412415
by_section[section] = []
413416
by_section[section].append(item)
414417

415-
console.print(f"\n[dim]Total Items: {len(items_for_totals)} across {len(by_section)} section(s)[/dim]")
418+
console.print(
419+
f"\n[dim]Total Items: {len(items_for_totals)} across {len(by_section)} section(s)[/dim]"
420+
)
416421

417422
# Display each section
418423
for section_name, section_items in by_section.items():
419424
# Section header with totals (items already filtered, no parents)
420425
section_value = sum(item.get("value", {}).get("amount", 0) or 0 for item in section_items)
421-
section_cost = sum(item.get("cost", {}).get("amount", 0) or 0 for item in section_items if "cost" in item)
426+
section_cost = sum(
427+
item.get("cost", {}).get("amount", 0) or 0 for item in section_items if "cost" in item
428+
)
422429

423430
console.print(f"\n[bold yellow]{section_name}[/bold yellow] ({len(section_items)} items)")
424431

@@ -439,7 +446,9 @@ def print_sheet_detail(
439446
# Detect which columns have data in this section
440447
has_ticker = any(item.get("ticker") for item in section_items)
441448
has_quantity = any(item.get("quantity") for item in section_items)
442-
has_cost = any("cost" in item and item.get("cost", {}).get("amount") for item in section_items)
449+
has_cost = any(
450+
"cost" in item and item.get("cost", {}).get("amount") for item in section_items
451+
)
443452

444453
# Create table for this section with only relevant columns
445454
table = Table(show_header=True, show_lines=False, box=None)
@@ -490,7 +499,10 @@ def print_sheet_detail(
490499
gain_color = "green" if gain >= 0 else "red"
491500
gain_sign = "+" if gain >= 0 else ""
492501

493-
gain_str = f"[{gain_color}]{gain_sign}{format_currency(gain, currency)}[/{gain_color}]"
502+
gain_str = (
503+
f"[{gain_color}]{gain_sign}"
504+
f"{format_currency(gain, currency)}[/{gain_color}]"
505+
)
494506
gain_pct_str = f"[{gain_color}]{gain_sign}{gain_pct:.2f}%[/{gain_color}]"
495507

496508
row.extend([cost_str, gain_str, gain_pct_str])

pytest.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@ addopts =
77
-v
88
--strict-markers
99
--tb=short
10-
--asyncio-mode=auto
1110
markers =
1211
asyncio: marks tests as async (deselect with '-m "not asyncio"')

0 commit comments

Comments
 (0)