Fix pyright type errors across codebase

- Add pyrightconfig.json for pyright 1.1.408 TOML parsing workaround
- Add NoReturn annotations to fatal() functions for proper type narrowing
- Add None checks and assertions after require=True get_record() calls
- Fix AttrDict class with __getattr__ for dynamic attribute access
- Add type annotations and casts for Kubernetes client objects
- Store compose config as DockerDeployer instance attributes
- Filter None values from dotenv and environment mappings
- Use hasattr/getattr patterns for optional container attributes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
A. F. DudleyandClaude Opus 4.5 committed 2026-01-22 01:10:36 -05:00
1 parent cd3d908d0d
commit dd856af2d3
29 files changed
+512 -267

No files matched your search

+81 -42
View File
@@ -25,6 +25,7 @@ import uuid
import yaml
from enum import Enum
from typing import Any, List, Optional, TextIO
from stack_orchestrator.deploy.webapp.registry_mutex import registry_mutex
@@ -41,27 +42,35 @@ AUCTION_KIND_PROVIDER = "provider"
class AttrDict(dict):
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self
def __getattribute__(self, attr):
def __getattribute__(self, attr: str) -> Any:
__dict__ = super(AttrDict, self).__getattribute__("__dict__")
if attr in __dict__:
v = super(AttrDict, self).__getattribute__(attr)
if isinstance(v, dict):
return AttrDict(v)
return v
return super(AttrDict, self).__getattribute__(attr)
def __getattr__(self, attr: str) -> Any:
# This method is called when attribute is not found
# Return None for missing attributes (matches original behavior)
return None
class TimedLogger:
def __init__(self, id="", file=None):
def __init__(self, id: str = "", file: Optional[TextIO] = None) -> None:
self.start = datetime.datetime.now()
self.last = self.start
self.id = id
self.file = file
def log(self, msg, show_step_time=True, show_total_time=False):
def log(
self, msg: str, show_step_time: bool = True, show_total_time: bool = False
) -> None:
prefix = f"{datetime.datetime.utcnow()} - {self.id}"
if show_step_time:
prefix += f" - {datetime.datetime.now() - self.last} (step)"
@@ -79,7 +88,7 @@ def load_known_requests(filename):
return {}
def logged_cmd(log_file, *vargs):
def logged_cmd(log_file: Optional[TextIO], *vargs: str) -> str:
result = None
try:
if log_file:
@@ -88,17 +97,22 @@ def logged_cmd(log_file, *vargs):
result.check_returncode()
return result.stdout.decode()
except Exception as err:
if result:
print(result.stderr.decode(), file=log_file)
else:
print(str(err), file=log_file)
if log_file:
if result:
print(result.stderr.decode(), file=log_file)
else:
print(str(err), file=log_file)
raise err
def match_owner(recordA, *records):
def match_owner(
recordA: Optional[AttrDict], *records: Optional[AttrDict]
) -> Optional[str]:
if not recordA or not recordA.owners:
return None
for owner in recordA.owners:
for otherRecord in records:
if owner in otherRecord.owners:
if otherRecord and otherRecord.owners and owner in otherRecord.owners:
return owner
return None
@@ -226,25 +240,27 @@ class LaconicRegistryClient:
]
# Most recent records first
results.sort(key=lambda r: r.createTime)
results.sort(key=lambda r: r.createTime or "")
results.reverse()
self._add_to_cache(results)
return results
def _add_to_cache(self, records):
def _add_to_cache(self, records: List[AttrDict]) -> None:
if not records:
return
for p in records:
self.cache["name_or_id"][p.id] = p
if p.id:
self.cache["name_or_id"][p.id] = p
if p.names:
for lrn in p.names:
self.cache["name_or_id"][lrn] = p
if p.attributes and p.attributes.type:
if p.attributes.type not in self.cache:
self.cache[p.attributes.type] = []
self.cache[p.attributes.type].append(p)
attr_type = p.attributes.type
if attr_type not in self.cache:
self.cache[attr_type] = []
self.cache[attr_type].append(p)
def resolve(self, name):
if not name:
@@ -556,26 +572,36 @@ def determine_base_container(clone_dir, app_type="webapp"):
return base_container
def build_container_image(app_record, tag, extra_build_args=None, logger=None):
def build_container_image(
app_record: Optional[AttrDict],
tag: str,
extra_build_args: Optional[List[str]] = None,
logger: Optional[TimedLogger] = None,
) -> None:
if app_record is None:
raise ValueError("app_record cannot be None")
if extra_build_args is None:
extra_build_args = []
tmpdir = tempfile.mkdtemp()
# TODO: determine if this code could be calling into the Python git
# library like setup-repositories
log_file = logger.file if logger else None
try:
record_id = app_record["id"]
ref = app_record.attributes.repository_ref
repo = random.choice(app_record.attributes.repository)
clone_dir = os.path.join(tmpdir, record_id)
logger.log(f"Cloning repository {repo} to {clone_dir} ...")
if logger:
logger.log(f"Cloning repository {repo} to {clone_dir} ...")
# Set github credentials if present running a command like:
# git config --global url."https://${TOKEN}:@github.com/".insteadOf
# "https://github.com/"
github_token = os.environ.get("DEPLOYER_GITHUB_TOKEN")
if github_token:
logger.log("Github token detected, setting it in the git environment")
if logger:
logger.log("Github token detected, setting it in the git environment")
git_config_args = [
"git",
"config",
@@ -583,9 +609,7 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
f"url.https://{github_token}:@github.com/.insteadOf",
"https://github.com/",
]
result = subprocess.run(
git_config_args, stdout=logger.file, stderr=logger.file
)
result = subprocess.run(git_config_args, stdout=log_file, stderr=log_file)
result.check_returncode()
if ref:
# TODO: Determing branch or hash, and use depth 1 if we can.
@@ -596,30 +620,32 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
subprocess.check_call(
["git", "clone", repo, clone_dir],
env=git_env,
stdout=logger.file,
stderr=logger.file,
stdout=log_file,
stderr=log_file,
)
except Exception as e:
logger.log(f"git clone failed. Is the repository {repo} private?")
if logger:
logger.log(f"git clone failed. Is the repository {repo} private?")
raise e
try:
subprocess.check_call(
["git", "checkout", ref],
cwd=clone_dir,
env=git_env,
stdout=logger.file,
stderr=logger.file,
stdout=log_file,
stderr=log_file,
)
except Exception as e:
logger.log(f"git checkout failed. Does ref {ref} exist?")
if logger:
logger.log(f"git checkout failed. Does ref {ref} exist?")
raise e
else:
# TODO: why is this code different vs the branch above (run vs check_call,
# and no prompt disable)?
result = subprocess.run(
["git", "clone", "--depth", "1", repo, clone_dir],
stdout=logger.file,
stderr=logger.file,
stdout=log_file,
stderr=log_file,
)
result.check_returncode()
@@ -627,7 +653,8 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
clone_dir, app_record.attributes.app_type
)
logger.log("Building webapp ...")
if logger:
logger.log("Building webapp ...")
build_command = [
sys.argv[0],
"--verbose",
@@ -643,10 +670,10 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
build_command.append("--extra-build-args")
build_command.append(" ".join(extra_build_args))
result = subprocess.run(build_command, stdout=logger.file, stderr=logger.file)
result = subprocess.run(build_command, stdout=log_file, stderr=log_file)
result.check_returncode()
finally:
logged_cmd(logger.file, "rm", "-rf", tmpdir)
logged_cmd(log_file, "rm", "-rf", tmpdir)
def push_container_image(deployment_dir, logger):
@@ -809,8 +836,12 @@ def skip_by_tag(r, include_tags, exclude_tags):
def confirm_payment(
laconic: LaconicRegistryClient, record, payment_address, min_amount, logger
):
laconic: LaconicRegistryClient,
record: AttrDict,
payment_address: str,
min_amount: int,
logger: TimedLogger,
) -> bool:
req_owner = laconic.get_owner(record)
if req_owner == payment_address:
# No need to confirm payment if the sender and recipient are the same account.
@@ -846,7 +877,8 @@ def confirm_payment(
)
return False
pay_denom = "".join([i for i in tx.amount if not i.isdigit()])
tx_amount = tx.amount or ""
pay_denom = "".join([i for i in tx_amount if not i.isdigit()])
if pay_denom != "alnt":
logger.log(
f"{record.id}: {pay_denom} in tx {tx.hash} is not an expected "
@@ -854,7 +886,7 @@ def confirm_payment(
)
return False
pay_amount = int("".join([i for i in tx.amount if i.isdigit()]))
pay_amount = int("".join([i for i in tx_amount if i.isdigit()]) or "0")
if pay_amount < min_amount:
logger.log(
f"{record.id}: payment amount {tx.amount} is less than minimum {min_amount}"
@@ -870,7 +902,8 @@ def confirm_payment(
used_request = laconic.get_record(used[0].attributes.request, require=True)
# Check that payment was used for deployment of same application
if record.attributes.application != used_request.attributes.application:
used_app = used_request.attributes.application if used_request else None
if record.attributes.application != used_app:
logger.log(
f"{record.id}: payment {tx.hash} already used on a different "
f"application deployment {used}"
@@ -890,8 +923,12 @@ def confirm_payment(
def confirm_auction(
laconic: LaconicRegistryClient, record, deployer_lrn, payment_address, logger
):
laconic: LaconicRegistryClient,
record: AttrDict,
deployer_lrn: str,
payment_address: str,
logger: TimedLogger,
) -> bool:
auction_id = record.attributes.auction
auction = laconic.get_auction(auction_id)
@@ -906,7 +943,9 @@ def confirm_auction(
auction_app = laconic.get_record(
auction_records_by_id[0].attributes.application, require=True
)
if requested_app.id != auction_app.id:
requested_app_id = requested_app.id if requested_app else None
auction_app_id = auction_app.id if auction_app else None
if requested_app_id != auction_app_id:
logger.log(
f"{record.id}: requested application {record.attributes.application} "
f"does not match application from auction record "