Skip to content
Merged
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ cd src && for f in aws_analyzer/[!_]*.py aws_analyzer/[!_]*/__init__.py; do m="$
makes (`monkeypatch.setattr(s3mod, "_require", ...)`, `monkeypatch.setattr(chatmod, "_cell_number", ...)`: the
chat's View asks it), on the `_kit` (or `_bedrock`) module for a call it makes
(`monkeypatch.setattr(viewmod, "_progress_bar_class", ...)` with `from aws_analyzer._kit import view as viewmod`:
`_BaseView._progress` makes that call, and `_BaseView.__init__` asks `_in_notebook` there too; an explorer's
once-per-cell display asks `_cell_number` on `_kit.window`).
`_ProgressLine`, the line `_BaseView._progress` shows, makes that call, and `_BaseView.__init__` asks
`_in_notebook` there too; an explorer's once-per-cell display asks `_cell_number` on `_kit.window`).
The exception is a companion (`COMPANIONS` in `.claude/skills/check/rules.py`): `s3_explorer.py` imports `s3`
(lazily, inside `_s3_module()`), reuses its helpers instead of copying them. Its
public commands are guarded by `_kit`'s `@_window_errors`, through its `_ready` (nothing runs without s3.py) and
Expand Down
7 changes: 0 additions & 7 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@ select = ["E4", "E7", "E9", "F", "B006", "BLE001", "ERA001", "C901", "PLR0912",
".claude/skills/demo/shots.py" = ["C901", "PLR0912", "PLR0913", "PLR0915"]
".claude/skills/release/release.py" = ["BLE001", "C901", "PLR0912", "PLR0915"]
"src/aws_analyzer/_bedrock.py" = ["C901", "PLR0912", "PLR0915"]
"src/aws_analyzer/_kit/fmt.py" = ["C901"]
"src/aws_analyzer/_kit/highlight.py" = ["C901"]
"src/aws_analyzer/_kit/jsontree.py" = ["PLR0913"]
"src/aws_analyzer/_kit/progress.py" = ["BLE001"]
"src/aws_analyzer/_kit/render.py" = ["C901", "PLR0912", "PLR0915"]
"src/aws_analyzer/_kit/view.py" = ["BLE001", "C901", "PLR0915"]
"src/aws_analyzer/_kit/window.py" = ["BLE001", "C901", "PLR0913"]
"src/aws_analyzer/agent/analyzer.py" = ["PLR0913"]
"src/aws_analyzer/agent/harness.py" = ["BLE001", "C901", "PLR0913"]
"src/aws_analyzer/agent/llm.py" = ["PLR0913"]
Expand Down
27 changes: 18 additions & 9 deletions src/aws_analyzer/_kit/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,40 @@ class _BaseAnalyzer:
"""
What every <Service>Analyzer starts with: a boto3 session (session=, or one made from region= and profile=),
the retry settings its clients use (self._config), and its prices, the service's list prices with prices= over
them. Nothing here talks to AWS until the service asks.
them (a copy, so the module's price table never changes). Nothing here talks to AWS until the service asks.

What the service sets:

_LIST_PRICES its price table (S3_PRICES, ...), which prices= overrides
client its main client
_clients the clients _cached_client made, by service name
_lock held while _cached_client makes a client
max_workers how many threads _map uses; 1 runs in order, as a Stubber answers
"""

_LIST_PRICES: dict[str, Any] = {} # the service's price table (S3_PRICES, ...), which prices= overrides
client: Any # the service's main client, which the service makes
_clients: dict[Any, Any] # _cached_client's clients by service name (the service sets it up, with its _lock)
_lock: Any # held while _cached_client makes a client: boto3 sessions aren't thread-safe
max_workers: int # how many threads _map uses (the service sets it); 1 runs in order, as a Stubber answers
_LIST_PRICES: dict[str, Any] = {}
client: Any
_clients: dict[Any, Any]
_lock: Any
max_workers: int

def __init__(self, session: Any = None, *, region: str | None = None, profile: str | None = None,
prices: dict[str, Any] | None = None):
self.session = session or boto3.Session(profile_name=profile, region_name=region)
self._config = Config(retries={"max_attempts": 10, "mode": "adaptive"}, max_pool_connections=50)
self.prices = {**self._LIST_PRICES, **(prices or {})} # a copy: the module's table never changes
self.prices = {**self._LIST_PRICES, **(prices or {})}

@property
def region(self) -> str:
return self.client.meta.region_name

def _cached_client(self, service: str, make: Callable[[], Any]) -> Any:
"""
The client for `service`, made by make() the first time it's needed (or the one passed in clients=).
The client for `service`, made by make() the first time it's needed (or the one passed in clients=). It's
made under the lock: boto3 sessions aren't thread-safe, and the windows read from threads.
"""
if service not in self._clients:
with self._lock: # sessions aren't thread-safe, and the windows read from threads
with self._lock:
if service not in self._clients:
self._clients[service] = make()
return self._clients[service]
Expand Down
111 changes: 82 additions & 29 deletions src/aws_analyzer/_kit/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
each block by what's registered for its type: its HTML, or its lines of text. These are the blocks every service
uses (render.py draws them); a service's own blocks (_Passage, _Steps, _Wiring, ...) are defined in its module and
register how they're drawn with @_draws when it's imported.

The registries @_draws fills: _HTML (block type -> its HTML), _TEXT (block type -> its lines in text mode) and
_CELLS (table cell type -> its HTML, given the cell's text).
"""

from __future__ import annotations
Expand All @@ -15,66 +18,110 @@

@dataclass
class _Title:
"""
The report's title, with a line under it (sub).
"""

text: str
sub: str = ""


@dataclass
class _Cards:
items: list[tuple[str, ...]] # (label, value), or (label, value, tone) with tone 'warn' | 'bad' | 'ok'
"""
The few numbers that matter, as cards. Each item is (label, value), or (label, value, tone) with tone 'warn',
'bad' or 'ok'.
"""

items: list[tuple[str, ...]]


@dataclass
class _Table:
"""
A table of rows under its headers.

rows cells are text, or _Tone for a coloured status (or another type registered as a cell)
bars 0..1 per row, drawn as an extra column headed bar_label
tree the first column holds indented tree labels
max_rows None for the view's default, 0 for no cap
code_cols columns holding calls to copy, shown as code
prose_cols columns of sentences this tool wrote (findings): the calls in them shown as code
collapsed a secondary view: folded under its title in HTML
path_cols columns of keys and paths: the file name always shows, a long folder is shortened
sortable in HTML a click on a column's header sorts by it (tree tables never sort)
"""

headers: list[str]
rows: list[list[Any]] # cells are text, or _Tone for a coloured status (or another type registered as a cell)
rows: list[list[Any]]
title: str = ""
bars: list[float] | None = None # 0..1 per row, drawn as an extra column
bars: list[float] | None = None
bar_label: str = "Share"
tree: bool = False # first column holds indented tree labels
max_rows: int | None = None # None = view default, 0 = no cap
code_cols: tuple[int, ...] = () # columns holding calls to copy, shown as code
prose_cols: tuple[int, ...] = () # columns of sentences this tool wrote (findings): calls in them shown as code
collapsed: bool = False # a secondary view: folded under its title in HTML
path_cols: tuple[int, ...] = () # columns of keys / paths: the file name always shows, a long folder is shortened
sortable: bool = True # in HTML a click on a column's header sorts by it (tree tables never sort)
tree: bool = False
max_rows: int | None = None
code_cols: tuple[int, ...] = ()
prose_cols: tuple[int, ...] = ()
collapsed: bool = False
path_cols: tuple[int, ...] = ()
sortable: bool = True


@dataclass
class _Note:
"""
A sentence on its own, at a level: 'info', 'warn' or 'ok'.
"""

text: str
level: str = "info" # 'info' | 'warn' | 'ok'
level: str = "info"


@dataclass
class _Text:
"""
Text shown as it is.

wrap prose: wrap long lines instead of scrolling sideways
code a snippet to copy: in HTML one click selects all of it
collapsed a secondary view (raw JSON): folded under its title in HTML
"""

text: str
title: str = ""
wrap: bool = False # prose: wrap long lines instead of scrolling sideways
code: bool = False # a snippet to copy: in HTML one click selects all of it
collapsed: bool = False # a secondary view (raw JSON): folded under its title in HTML
wrap: bool = False
code: bool = False
collapsed: bool = False


@dataclass
class _Findings:
items: list[tuple[str, str]] # (level, message) pairs from a *_findings function
empty: str = "" # said (as an ok note) when there are none; nothing when blank
"""
The (level, message) pairs from a *_findings function, and what to say (as an ok note) when there are none;
nothing when `empty` is blank.
"""

items: list[tuple[str, str]]
empty: str = ""


@dataclass
class _Next:
items: list[tuple[str, str]] # (call, what it shows): the commands worth running next, arguments filled in
"""
The commands worth running next, as (call, what it shows), with their arguments filled in.
"""

items: list[tuple[str, str]]
title: str = "Next"


@dataclass
class _Tone:
"""
A table cell with a status colour: a pill in HTML, plain text elsewhere.
A table cell with a status colour (tone: 'warn', 'bad' or 'ok'): a pill in HTML, plain text elsewhere.
"""

text: str
tone: str = "warn" # 'warn' | 'bad' | 'ok'
tone: str = "warn"

def __str__(self) -> str:
return self.text
Expand All @@ -84,15 +131,21 @@ def __str__(self) -> str:
class _Report:
"""
One report being drawn: what each block's drawing function gets besides the block.

max_rows the view's cap on table rows; 0 for no cap
badge the chip before each title (HTML), so reports from different services are easy to tell apart
root the service's root class, which scopes the CSS rules
blocks the whole report, for a block that needs to know what else is in it
rules CSS rules the blocks need beyond the stylesheet (a table's filters and columns): a <style> after it
parts what blocks share while the report is drawn (see shared)
"""

max_rows: int # the view's cap on table rows; 0 = no cap
badge: str = "" # the chip before each title (HTML), so reports from different services are easy to tell apart
root: str = "" # the service's root class, which scopes the CSS rules below
blocks: tuple[Any, ...] = () # the whole report, for a block that needs to know what else is in it
rules: set[str] = field(default_factory=set) # CSS rules the blocks need beyond the stylesheet (a table's
# filters and columns): a <style> after it
parts: dict[Any, Any] = field(default_factory=dict) # what blocks share while the report is drawn (see shared)
max_rows: int
badge: str = ""
root: str = ""
blocks: tuple[Any, ...] = ()
rules: set[str] = field(default_factory=set)
parts: dict[Any, Any] = field(default_factory=dict)

def shared(self, key: Any, make: Callable[[], Any]) -> Any:
"""
Expand All @@ -104,9 +157,9 @@ def shared(self, key: Any, make: Callable[[], Any]) -> Any:
return self.parts[key]


_HTML: dict[type, Callable[[Any, _Report], str]] = {} # block type -> its HTML
_TEXT: dict[type, Callable[[Any, _Report], list[str]]] = {} # block type -> its lines in text mode
_CELLS: dict[type, Callable[[Any, str], str]] = {} # table cell type -> its HTML, given the cell's text
_HTML: dict[type, Callable[[Any, _Report], str]] = {}
_TEXT: dict[type, Callable[[Any, _Report], list[str]]] = {}
_CELLS: dict[type, Callable[[Any, str], str]] = {}


def _draws(kind: type, mode: str) -> Callable[[Callable], Callable]:
Expand Down
Loading
Loading