-
Notifications
You must be signed in to change notification settings - Fork 6
.pr_agent_auto_best_practices
Pattern 1: When implementing an apply()-style upsert, only fall back to create() on a true "not found" condition (HTTP 404 / explicit NotFound), and re-raise all other errors (403/500/etc) to avoid masking real failures.
Example code before:
def apply(self, name, body):
try:
self.find(name)
return self.update(name, body)
except DuploError:
return self.create(body)
Example code after:
def apply(self, name, body):
try:
self.find(name)
return self.update(name, body)
except DuploError as e:
if getattr(e, "code", None) == 404:
return self.create(body)
raise
Relevant past accepted suggestions:
Suggestion 1:
[reliability] `apply` catches all `DuploError`
`apply` catches all `DuploError`
`apply()` falls back to `create()` for any `DuploError` thrown by `find()`/`update()`, which can mask real failures like 403/500. Only the intended not-found condition (e.g., 404) should trigger create; other errors should be re-raised.aws_secret.apply() catches all DuploError exceptions and falls back to create(), which can hide authorization/server errors and perform unintended creates.
Compliance requires fallback behavior only for the intended condition (typically 404 Not Found). Other status codes must be re-raised.
- src/duplo_resource/aws_secret.py[221-225]
Suggestion 2:
[correctness] Apply misses 404 errors
Apply misses 404 errors
`DuploResourceV2.apply()`/`DuploResourceV3.apply()` now catch only `DuploNotFound`, so resources whose `find()` raises `DuploError(..., 404)` will cause `apply()` to error instead of falling back to `create()`. This breaks `apply` for resources like `batch_queue` (custom `find`) and `ecr` (delegates not-found to `cloud_resource.find()` which raises `DuploError(404)`).DuploResourceV2.apply() and DuploResourceV3.apply() now catch only DuploNotFound. Many resources still raise DuploError(..., 404) from custom find() implementations, so apply() no longer falls back to create() for them.
Examples include batch_queue.find() and cloud_resource.find() (used by ecr.find()), both raising DuploError(404).
- src/duplocloud/resource.py[143-152]
- src/duplocloud/resource.py[335-340]
- src/duplo_resource/batch_queue.py[48-70]
- src/duplo_resource/cloud_resource.py[63-95]
- src/duplo_resource/batch_job.py[59-84]
Preferred approach: in both apply methods catch DuploError as e and only create when e.code == 404 (re-raise otherwise). Optionally, migrate custom find() methods to raise DuploNotFound(name, kind) for consistency.
Suggestion 3:
[reliability] Apply masks non-404 errors
Apply masks non-404 errors
DuploHosts.apply catches any DuploError from find and falls back to create, so auth/permission/server errors during list/find can incorrectly be treated as “not found,” triggering unwanted create attempts and hiding the real failure.hosts.apply() currently treats any DuploError thrown by hosts.find() as “host does not exist” and calls create(). This can incorrectly create resources (or attempt to) when the underlying failure is an auth/permission/server error.
-
DuploAPIraisesDuploErrorfor 401/403/400/5xx. -
hosts.find()callslist()which can surface those errors.
- src/duplo_resource/hosts.py[71-77]
- src/duplocloud/client.py[148-164]
Pattern 2: Validate optional CLI inputs (e.g., body, name, required keys) before use and raise a clear domain error instead of allowing AttributeError/KeyError/TypeError from assuming shapes like dict or required fields being present.
Example code before:
def name_from_body(body):
return body["SecretName"] # KeyError if missing
def create(body=None):
body.setdefault("metadata", {}) # AttributeError if body is None
Example code after:
def name_from_body(body):
if not isinstance(body, dict) or "SecretName" not in body:
raise DuploError("SecretName is required in request body", 400)
return body["SecretName"]
def create(body=None):
if body is None or not isinstance(body, dict):
raise DuploError("Body is required; pass -f/--cli-input with a JSON/YAML object", 400)
body.setdefault("metadata", {})
Relevant past accepted suggestions:
Suggestion 1:
[correctness] Missing body crashes commands
Missing body crashes commands
`argo_wf` and `argo_wf_template` commands call `_ensure_namespace(body, ...)` assuming `body` is a dict; if the user omits `-f/--cli-input`, `args.BODY` remains `None` and the code raises `AttributeError` instead of a `DuploError`. This affects `create/apply` (workflows) and `create/update/apply` (templates).argo_wf / argo_wf_template commands crash with AttributeError when invoked without -f/--cli-input (or when the YAML root is not a mapping), because _ensure_namespace() assumes body is a dict and calls setdefault().
-
args.BODYis an optional flag and can beNone. -
YamlActioncan also parse YAML into non-dict types.
- src/duplo_resource/argo_wf.py[19-33]
- src/duplo_resource/argo_wf.py[102-189]
- src/duplo_resource/argo_wf.py[341-463]
- Add a guard in
_ensure_namespace(): - if
not isinstance(body, dict): raiseDuploError("Body is required; pass -f/--cli-input with a YAML/JSON object", 400) - optionally validate
body.get(key)is dict when present. - (Optional) Add small unit tests for missing body behavior to ensure it raises
DuploErrorinstead ofAttributeError.
Suggestion 2:
[reliability] `name_from_body()` missing validation
`name_from_body()` missing validation
`name_from_body()` directly accesses `body["SecretName"]` without validating `body` shape, which can raise `KeyError` and break `apply()` on malformed/partial inputs. This violates the requirement to explicitly handle null/empty/missing fields in edge cases.name_from_body() can raise KeyError when SecretName is missing, which is an unhandled edge case for apply().
apply() depends on name_from_body(body); missing/invalid inputs should fail gracefully with a clear DuploError message.
- src/duplo_resource/secret.py[21-22]
- src/duplo_resource/secret.py[223-253]
Suggestion 3:
[correctness] Tenant start/stop crash
Tenant start/stop crash
DuploHosts.name_from_body now returns None when FriendlyName is missing, but tenant.start/tenant.stop call service.name_from_body(item) and then invoke hosts.start/hosts.stop with that value (and an extra positional arg), which can raise TypeError/AttributeError at runtime instead of skipping unnamed hosts.DuploHosts.name_from_body() can now return None when a host list item has no FriendlyName. tenant start/tenant stop currently assumes service.name_from_body(item) is always a string and unconditionally calls service.start(...) / service.stop(...), which for hosts can crash (passing None as the name and also passing an extra positional arg).
- Host list API may return items without
FriendlyName. - Tenant workflows iterate all hosts and invoke lifecycle operations.
- src/duplo_resource/tenant.py[498-504]
- src/duplo_resource/tenant.py[553-559]
- src/duplo_resource/hosts.py[309-316]
Pattern 3: Before calling response.json(), explicitly handle empty responses (e.g., HTTP 204 or empty content) by returning {}/None as appropriate to avoid JSONDecodeError during normal delete/request flows.
Example code before:
resp = client.delete(url)
return resp.json()
Example code after:
resp = client.delete(url)
if resp.status_code == 204 or not resp.content:
return {}
return resp.json()
Relevant past accepted suggestions:
Suggestion 1:
[reliability] `delete()` parses empty JSON
`delete()` parses empty JSON
`delete()` unconditionally calls `response.json()`, which can fail for 204/empty delete responses. This can cause runtime errors during normal delete operations.delete() paths for Argo workflow resources call .json() unconditionally. For typical delete semantics, servers may return 204 No Content or a 2xx with an empty body, which makes response.json() raise.
Compliance requires checking for 204/empty responses before JSON parsing.
- src/duplo_resource/argo_wf.py[146-149]
- src/duplo_resource/argo_wf.py[428-431]
- src/duplo_resource/argo_client.py[144-164]
Suggestion 2:
Handle empty API responses gracefully
In the argo_request method, check for empty response bodies or a 204 status code before attempting to parse JSON to prevent a JSONDecodeError.
src/duplo_resource/argo_wf.py [174-177]
if not (200 <= response.status_code < 300):
raise DuploError(f"Argo API error: {response.text}", response.status_code)
+if response.status_code == 204 or not response.content:
+ return {}
return response.json()Pattern 4: Consolidate duplicated logic (prefix handling, normalization, repeated blocks, duplicate helper functions) into a single helper and reuse it everywhere to keep behavior consistent and reduce drift.
Example code before:
def find(name):
if not name.startswith("pre-"):
name = "pre-" + name
...
def delete(name):
if not name.startswith("pre-"):
name = "pre-" + name
...
Example code after:
def _ensure_prefix(name, prefix="pre-"):
return name if name.startswith(prefix) else prefix + name
def find(name):
name = _ensure_prefix(name)
...
def delete(name):
name = _ensure_prefix(name)
...
Relevant past accepted suggestions:
Suggestion 1:
Extract image normalization helper method
Extract the image normalization logic into a shared helper method to avoid duplication and ensure consistent image processing across the codebase.
src/duplo_resource/service.py [710-716]
+def _normalize_image(self, image):
+ """Normalize image name by removing Docker registry prefixes."""
+ return image.removeprefix("docker.io/library/").removeprefix("docker.io/").removeprefix("library/")
+
def get_pod_image(pod):
"""Get the image of a pod."""
- raw_image=pod["Containers"][0]["Image"]
+ raw_image = pod["Containers"][0]["Image"]
self.duplo.logger.debug(f"Retrieved raw image {raw_image} for {pod['InstanceId']}")
- normalized_image = raw_image.removeprefix("docker.io/library/").removeprefix("docker.io/").removeprefix("library/")
+ normalized_image = self._normalize_image(raw_image)
self.duplo.logger.debug(f"Returning normalized image {normalized_image} for {pod['InstanceId']}\n")
return normalized_imageSuggestion 2:
Refactor duplicated prefix logic
The prefix logic is duplicated in both find() and delete() methods. This could lead to inconsistencies if the prefix format changes in one place but not the other. Extract this logic into a helper method to ensure consistent behavior.
src/duplo_resource/aws_secret.py [51-54]
-# if the name has the prefix we good, otherwise add it
-prefix = f"duploservices-{self.duplo.tenant}-"
-if not name.startswith(prefix):
- name = prefix + name
+# Use a helper method to ensure consistent prefix handling
+name = self._ensure_name_has_prefix(name)Suggestion 3:
Remove duplicate function definition
This function redefines the get_test_data function that's already imported at the top of the file. This duplication can lead to confusion about which function is being used. Remove this duplicate definition and use the imported function instead.
src/tests/test_cloudfront.py [23-27]
-def get_test_data(name) -> dict:
- dir = pathlib.Path(__file__).parent.resolve()
- f = f"{dir}/data/{name}.yaml"
- with open(f, 'r') as stream:
- return yaml.safe_load(stream)
+# Remove this duplicate function definition entirely, as it's already imported
+# from .conftest import get_test_dataPattern 5: Keep user-facing documentation and messages accurate and professional by fixing typos/grammar, ensuring examples reference the correct command/resource, and keeping docstrings aligned with actual function signatures/behavior.
Example code before:
"""Update a secret.
Usage:
duploctl ssm_param update <name> -pval <newvalue>
Returns:
AI Agent reponse & a Chat url.
"""
Example code after:
"""Update a secret.
Usage:
duploctl aws_secret update <name> -pval <newvalue>
Returns:
AI Agent response and a chat URL.
"""
Relevant past accepted suggestions:
Suggestion 1:
[correctness] CHANGELOG text has typos
CHANGELOG text has typos
New release notes contain multiple typos/grammar issues in user-facing documentation. This can confuse users and reduces documentation quality.The newly added CHANGELOG entry contains multiple typos/grammar issues in user-facing release notes (e.g., introducses, entrypoiint, registrered).
Compliance requires documentation and user-facing strings to be correct and consistent to avoid confusing users.
- CHANGELOG.md[20-24]
Suggestion 2:
Correct duplicated and unclear docstring
Remove duplicated sentences and ensure consistent, concise phrasing. Adjust articles and capitalization in prose for clarity and correctness.
src/duplo_resource/service.py [626-653]
"""Start a service.
-
-Start a service.
Usage: Basic CLI Use
```sh
duploctl service start <service-name>
duploctl service start --all
duploctl service start --targets service1 service2 service3-Example: Wait for Start
- This command supports the global --wait. Waits for the desired count of pods to become ready. +Example: Wait for start
- This command supports the global --wait flag. It waits for the desired number of pods to become ready.
duploctl service start myapp --wait --loglevel INFO
Args: name: The name of the service to start.
- all: Boolean flag to start all services. Defaults to False.
- all: Whether to start all services. Defaults to False. targets: List of service names to start. Cannot be used with name or all.
-Returns: +Returns: A summary containing services that were started successfully and those that encountered errors.
Raises: DuploError: If the service could not be started. """
___
<b>
Suggestion 3</b>:
Fix incorrect resource name in documentation example
**The update method's docstring incorrectly refers to 'ssm_param' instead of 'aws_secret' in the CLI usage example.**
[src/duplo_resource/aws_secret.py [103]](https://github.com/duplocloud/duploctl/pull/118/files#diff-34abfd7c842718be581a6b194282f9a51a493d1c984ce3310b77316480f500feR103-R103)
```diff
"""Update a secret.
Usage: cli usage
```sh
- duploctl ssm_param update <name> -pval <newvalue>
+ duploctl aws_secret update <name> -pval <newvalue>
- [ ] **Apply this suggestion**
</details>
___
`[Auto-generated best practices - 2026-06-18]`