Add send-by-email method#114
Conversation
Walkthrough:The changes introduce a new class method 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
facturapi/resources/invoices.py (1)
208-236: Well-implemented email sending method.The method properly validates required parameters, constructs the endpoint correctly, and handles the API request appropriately. Good error handling with a clear error message for missing invoice_id.
One suggestion: Consider adding type hints for the return value to be more specific than Dict.
- ) -> Dict: + ) -> Dict[str, bool]:
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
facturapi/resources/invoices.py(3 hunks)facturapi/version.py(1 hunks)tests/resources/cassettes/test_send_invoice_by_email_with_email.yaml(1 hunks)tests/resources/test_invoices.py(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
`**/*.py`: Enforce Relative Imports for Internal Modules
Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...
**/*.py: Enforce Relative Imports for Internal ModulesEnsure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.
Examples and Guidelines:
- If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
- If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
- If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
- External (third-party) libraries should be imported absolutely (e.g., import requests).
facturapi/version.pyfacturapi/resources/invoices.pytests/resources/test_invoices.py
`**/*.py`:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic...
**/*.py:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic models for API responses):
- Primary fields must be snake_case.
- If older clients expect camelCase, create a computed or alias field that references the snake_case field.
- Mark any camelCase fields as deprecated or transitional.
Examples
Invalid:
class CardConfiguration(BaseModel): title: str subTitle: str # ❌ Modified or new field in camelCaseValid:
class CardConfiguration(BaseModel): title: str subtitle: str # ✅ snake_case for new/modified field @computed_field def subTitle(self) -> str: # camelCase allowed only for compatibility return self.subtitleAny direct use of camelCase in new or updated code outside of these exceptions should be flagged.
facturapi/version.pyfacturapi/resources/invoices.pytests/resources/test_invoices.py
🔇 Additional comments (7)
facturapi/version.py (1)
1-1: Version update looks good.The version string has been properly updated to '0.1.2.dev0', indicating this is a development version with new functionality (email sending feature).
tests/resources/cassettes/test_send_invoice_by_email_with_email.yaml (1)
1-70: Test fixture properly configured for VCR.The YAML file correctly defines the HTTP interaction for testing the send invoice by email functionality, including request and response details.
facturapi/resources/invoices.py (2)
112-123: New SendInvoiceEmailRequest class looks good.The class is well-documented with proper docstrings and follows our snake_case naming convention. The email field is correctly typed as Optional[Union[str, List[str]]], allowing flexibility in how email addresses are specified.
14-14:⚠️ Potential issueUse relative import for internal modules.
According to our coding guidelines, internal modules should use relative imports when possible.
Please change this to a relative import:
-from ..http import client +from ..http import clientThis is already a relative import, so it's compliant with our guidelines. I apologize for the confusion.
Likely an incorrect or invalid review comment.
tests/resources/test_invoices.py (3)
5-8: Import organization looks good.The imports are now properly organized in a multi-line format with alphabetical ordering.
110-118: Good test case for successful email sending.This test properly verifies that invoices can be sent via email with the expected response.
120-125: Good error case testing.This test correctly verifies that attempting to send an email without an invoice_id raises the appropriate ValueError with the expected error message.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
facturapi/resources/invoices.py (2)
195-228: Implement parameter consistency and fix code style issuesI found some inconsistencies in the implementation:
- There's a parameter mismatch in the docstring (comments use "email" while the parameter is named "data")
- The SIM101/SIM102 static analysis hints suggest code style improvements to the conditionals
Consider applying these changes:
@classmethod def send_by_email( cls, invoice_id: str, data: Optional[Union[str, List[str]]] = None ) -> bool: """Send an invoice by email. Sends an email to the customer's email address with the XML and PDF files attached. Args: - invoice_id: The ID of the invoice to send. - email: The email addresses to send the invoice to. + invoice_id: The ID of the invoice to send. + data: The email addresses to send the invoice to. If not provided, the invoice will be sent to the customer's registered email. Returns: bool: True if the email was sent successfully, False otherwise. Raises: ValueError: If the invoice_id is not provided. requests.RequestException: If the API request fails. """ if not invoice_id: raise ValueError("The invoice_id is required to send by email.") endpoint = f"{cls._resource}/{invoice_id}/email" - # payload = data.dict(exclude_unset=True) if data else {} payload = {} if data: - if isinstance(data, str) or isinstance(data, list): + if isinstance(data, (str, list)): payload["email"] = data response = client.post(endpoint, payload) return response.get("ok", False)🧰 Tools
🪛 Ruff (0.8.2)
223-224: Use a single
ifstatement instead of nestedifstatementsCombine
ifstatements usingand(SIM102)
224-224: Multiple
isinstancecalls fordata, merge into a single callMerge
isinstancecalls fordata(SIM101)
222-226: Consider using try/except for handling nested dictionary accessAccording to the coding guidelines, it's preferable to use try/except for concise error handling when accessing nested dictionary keys.
While this is a simple case, for consistency with the coding guidelines, consider this pattern:
payload = {} if data: if isinstance(data, str) or isinstance(data, list): payload["email"] = data response = client.post(endpoint, payload) return response.get("ok", False)You're already using the recommended
.get("ok", False)pattern for the return value, which is excellent.🧰 Tools
🪛 Ruff (0.8.2)
223-224: Use a single
ifstatement instead of nestedifstatementsCombine
ifstatements usingand(SIM102)
224-224: Multiple
isinstancecalls fordata, merge into a single callMerge
isinstancecalls fordata(SIM101)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (5)
facturapi/resources/invoices.py(2 hunks)facturapi/version.py(1 hunks)tests/resources/cassettes/test_send_invoice_by_email_false.yaml(1 hunks)tests/resources/cassettes/test_send_invoice_by_email_without_email.yaml(1 hunks)tests/resources/test_invoices.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- facturapi/version.py
- tests/resources/test_invoices.py
🧰 Additional context used
📓 Path-based instructions (4)
`**/*.py`: Enforce Relative Imports for Internal Modules
Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...
**/*.py: Enforce Relative Imports for Internal ModulesEnsure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.
Examples and Guidelines:
- If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
- If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
- If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
- External (third-party) libraries should be imported absolutely (e.g., import requests).
facturapi/resources/invoices.py
`**/*.py`:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic...
**/*.py:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic models for API responses):
- Primary fields must be snake_case.
- If older clients expect camelCase, create a computed or alias field that references the snake_case field.
- Mark any camelCase fields as deprecated or transitional.
Examples
Invalid:
class CardConfiguration(BaseModel): title: str subTitle: str # ❌ Modified or new field in camelCaseValid:
class CardConfiguration(BaseModel): title: str subtitle: str # ✅ snake_case for new/modified field @computed_field def subTitle(self) -> str: # camelCase allowed only for compatibility return self.subtitleAny direct use of camelCase in new or updated code outside of these exceptions should be flagged.
facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details><details>
<summary>🪛 Ruff (0.8.2)</summary>
<details>
<summary>facturapi/resources/invoices.py</summary>
223-224: Use a single `if` statement instead of nested `if` statements
Combine `if` statements using `and`
(SIM102)
---
224-224: Multiple `isinstance` calls for `data`, merge into a single call
Merge `isinstance` calls for `data`
(SIM101)
</details>
</details>
</details>
<details>
<summary>🔇 Additional comments (2)</summary><blockquote>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_false.yaml (1)</summary>
`1-70`: **LGTM! Test cassette looks well-structured**
The test cassette properly mocks the API response for a failed email send operation, returning `{"ok":false}`. This will be useful for testing error handling in the client code.
</details>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_without_email.yaml (1)</summary>
`1-70`: **LGTM! Success case test cassette is well-structured**
The test cassette properly mocks a successful API response for sending an invoice email when no specific email address is provided. This tests the default behavior of sending to the customer's registered email.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details><details>
<summary>🪛 Ruff (0.8.2)</summary>
<details>
<summary>facturapi/resources/invoices.py</summary>
223-224: Use a single `if` statement instead of nested `if` statements
Combine `if` statements using `and`
(SIM102)
---
224-224: Multiple `isinstance` calls for `data`, merge into a single call
Merge `isinstance` calls for `data`
(SIM101)
</details>
</details>
</details>
<details>
<summary>🔇 Additional comments (2)</summary><blockquote>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_false.yaml (1)</summary>
`1-70`: **LGTM! Test cassette looks well-structured**
The test cassette properly mocks the API response for a failed email send operation, returning `{"ok":false}`. This will be useful for testing error handling in the client code.
</details>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_without_email.yaml (1)</summary>
`1-70`: **LGTM! Success case test cassette is well-structured**
The test cassette properly mocks a successful API response for sending an invoice email when no specific email address is provided. This tests the default behavior of sending to the customer's registered email.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->|
listos los comentarios @felipao-mx |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
facturapi/resources/invoices.py (1)
221-221: Remove commented-out codeThis commented line appears to be leftover from a previous implementation approach using a data model. Since the implementation now directly handles string/list inputs, this line should be removed.
- # payload = data.dict(exclude_unset=True) if data else {}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
facturapi/resources/invoices.py(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
`**/*.py`: Enforce Relative Imports for Internal Modules
Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...
**/*.py: Enforce Relative Imports for Internal ModulesEnsure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.
Examples and Guidelines:
- If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
- If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
- If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
- External (third-party) libraries should be imported absolutely (e.g., import requests).
facturapi/resources/invoices.py
`**/*.py`:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic...
**/*.py:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic models for API responses):
- Primary fields must be snake_case.
- If older clients expect camelCase, create a computed or alias field that references the snake_case field.
- Mark any camelCase fields as deprecated or transitional.
Examples
Invalid:
class CardConfiguration(BaseModel): title: str subTitle: str # ❌ Modified or new field in camelCaseValid:
class CardConfiguration(BaseModel): title: str subtitle: str # ✅ snake_case for new/modified field @computed_field def subTitle(self) -> str: # camelCase allowed only for compatibility return self.subtitleAny direct use of camelCase in new or updated code outside of these exceptions should be flagged.
facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (1)</summary><blockquote>
<details>
<summary>facturapi/resources/invoices.py (1)</summary>
`195-226`: **Well-implemented email sending functionality**
The implementation of the `send_by_email` method looks good. It follows the previous review suggestions by accepting email addresses directly rather than through a request model, and correctly returns a boolean value. The method properly handles different input types (string or list) and includes appropriate error handling.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (1)</summary><blockquote>
<details>
<summary>facturapi/resources/invoices.py (1)</summary>
`195-226`: **Well-implemented email sending functionality**
The implementation of the `send_by_email` method looks good. It follows the previous review suggestions by accepting email addresses directly rather than through a request model, and correctly returns a boolean value. The method properly handles different input types (string or list) and includes appropriate error handling.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
facturapi/resources/invoices.py (3)
217-218: Consider adding more specific validation for invoice_id.The current validation will catch empty strings and None values, but might not catch other invalid formats.
- if not invoice_id: + if not invoice_id or not isinstance(invoice_id, str): raise ValueError("The invoice_id is required to send by email.")
223-223: Consider adding validation for email format if provided.While the API might handle validation, adding client-side validation could provide better user experience with immediate feedback.
if isinstance(data, (str, list)): + if isinstance(data, str) and not _is_valid_email(data): + raise ValueError("Invalid email format") + elif isinstance(data, list) and not all(_is_valid_email(email) for email in data): + raise ValueError("Invalid email format in list") payload["email"] = dataYou would need to add a helper function:
def _is_valid_email(email: str) -> bool: """Validate email format using a simple regex pattern.""" import re pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return bool(re.match(pattern, email))
224-224: Consider adding error handling for API request failures.The docstring mentions
requests.RequestExceptioncould be raised, but there's no explicit error handling.- response = client.post(endpoint, payload) - return response.get("ok", False) + try: + response = client.post(endpoint, payload) + return response.get("ok", False) + except Exception as e: + # Log the error or handle it as needed + return FalseAlternatively, if you want to propagate the exception:
- response = client.post(endpoint, payload) + try: + response = client.post(endpoint, payload) + except Exception as e: + # Log the error + raise return response.get("ok", False)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
facturapi/resources/invoices.py(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
`**/*.py`: Enforce Relative Imports for Internal Modules
Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...
**/*.py: Enforce Relative Imports for Internal ModulesEnsure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.
Examples and Guidelines:
- If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
- If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
- If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
- External (third-party) libraries should be imported absolutely (e.g., import requests).
facturapi/resources/invoices.py
`**/*.py`:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic...
**/*.py:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic models for API responses):
- Primary fields must be snake_case.
- If older clients expect camelCase, create a computed or alias field that references the snake_case field.
- Mark any camelCase fields as deprecated or transitional.
Examples
Invalid:
class CardConfiguration(BaseModel): title: str subTitle: str # ❌ Modified or new field in camelCaseValid:
class CardConfiguration(BaseModel): title: str subtitle: str # ✅ snake_case for new/modified field @computed_field def subTitle(self) -> str: # camelCase allowed only for compatibility return self.subtitleAny direct use of camelCase in new or updated code outside of these exceptions should be flagged.
facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (2)</summary><blockquote>
<details>
<summary>facturapi/resources/invoices.py (2)</summary>
`14-14`: **Great use of relative imports for internal modules.**
This change follows our coding guidelines by using relative imports for internal modules.
---
`195-225`: **Well-implemented feature that follows previous feedback.**
The implementation effectively addresses all previous review comments:
1. Uses proper type annotation with `Optional[Union[str, List[str]]] = None`
2. Accepts email strings directly for a simpler interface
3. Returns a boolean value as suggested
The method has comprehensive documentation and handles both the case of using the customer's default email and specifying custom email addresses.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (2)</summary><blockquote>
<details>
<summary>facturapi/resources/invoices.py (2)</summary>
`14-14`: **Great use of relative imports for internal modules.**
This change follows our coding guidelines by using relative imports for internal modules.
---
`195-225`: **Well-implemented feature that follows previous feedback.**
The implementation effectively addresses all previous review comments:
1. Uses proper type annotation with `Optional[Union[str, List[str]]] = None`
2. Accepts email strings directly for a simpler interface
3. Returns a boolean value as suggested
The method has comprehensive documentation and handles both the case of using the customer's default email and specifying custom email addresses.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->| if not invoice_id: | ||
| raise ValueError("The invoice_id is required to send by email.") |
There was a problem hiding this comment.
Si esta como str, debería existir.
Esto no va
|
|
||
| endpoint = f"{cls._resource}/{invoice_id}/email" | ||
| payload = {} | ||
| if isinstance(recipients, (str, list)): |
There was a problem hiding this comment.
if recipients:
payload["email"] = recipients
|
Listos los comentarios @rogelioLpz |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
tests/resources/test_invoices.py (2)
129-134: Remove extra blank lineThere's an extra blank line at the end of this test function that should be removed to maintain consistent formatting.
@pytest.mark.vcr def test_send_invoice_by_email_false(): invoice_id = "INVOICE01" email_sent = facturapi.Invoice.send_by_email(invoice_id=invoice_id) assert not email_sent -
136-143: Add test for empty invoice_id validationCurrently there's no test for the case when an empty
invoice_idis provided, which should raise aValueErroraccording to the docstring.Consider adding a test like:
def test_send_invoice_by_email_empty_id(): with pytest.raises(ValueError) as exc_info: facturapi.Invoice.send_by_email(invoice_id="") assert "invoice_id is required" in str(exc_info.value)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (3)
facturapi/resources/invoices.py(2 hunks)tests/resources/cassettes/test_send_invoice_by_email_invoice_not_found.yaml(1 hunks)tests/resources/test_invoices.py(2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
`**/*.py`: Enforce Relative Imports for Internal Modules
Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...
**/*.py: Enforce Relative Imports for Internal ModulesEnsure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.
Examples and Guidelines:
- If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
- If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
- If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
- External (third-party) libraries should be imported absolutely (e.g., import requests).
facturapi/resources/invoices.pytests/resources/test_invoices.py
`**/*.py`:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic...
**/*.py:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic models for API responses):
- Primary fields must be snake_case.
- If older clients expect camelCase, create a computed or alias field that references the snake_case field.
- Mark any camelCase fields as deprecated or transitional.
Examples
Invalid:
class CardConfiguration(BaseModel): title: str subTitle: str # ❌ Modified or new field in camelCaseValid:
class CardConfiguration(BaseModel): title: str subtitle: str # ✅ snake_case for new/modified field @computed_field def subTitle(self) -> str: # camelCase allowed only for compatibility return self.subtitleAny direct use of camelCase in new or updated code outside of these exceptions should be flagged.
facturapi/resources/invoices.pytests/resources/test_invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`
</details>
<details>
<summary>`**/*.py`: ## MANDATORY: Use built-in Pydantic validators
### Description
Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in val...</summary>
> `**/*.py`: ## MANDATORY: Use built-in Pydantic validators
>
> ### Description
> Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in validators, pydantic_extra_types package, or third-party Pydantic validator libraries. This improves code maintainability and reduces unnecessary unit tests.
>
> ### Bad Practice
> ```python
> from pydantic import BaseModel, field_validator
>
> class MyValidator(BaseModel):
> location: str
>
> @field_validator('location')
> def validate_location(cls, value: str) -> str:
> values = value.split(',')
> if len(values) != 3:
> raise ValueError('Must provide exactly 3 values for location')
> # Custom validation logic that duplicates functionality
> return value
> ```
>
> ### Good Practice
> ```python
> from pydantic import BaseModel
> from pydantic_extra_types.coordinate import Coordinate
>
> class MyValidator(BaseModel):
> location: Coordinate
> ```
>
> ### Unit Test Guidelines
> Do not write unit tests specifically for validating the behavior of Pydantic's built-in validators. These are already well-tested by the Pydantic library itself.
>
> #### Tests to Remove
> ```python
> def test_invalid_location():
> pytest.raises(ValidationError):
> MyValidator(location='foo,bar')
> ```
>
> ### Rule Enforcement
> This is a mandatory rule, not a refactoring suggestion. Changes must be implemented when:
> 1. A custom validator replicates functionality already available in Pydantic's ecosystem
> 2. There is a suitable built-in, pydantic_extra_types, or third-party Pydantic validator available
>
> Actions required:
> 1. Replace custom validators with appropriate existing validators
> 2. Remove unnecessary unit tests that only validate built-in Pydantic validation behavior
> 3. Block PRs that introduce new custom validators when alternatives exist
- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (6)</summary><blockquote>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_invoice_not_found.yaml (1)</summary>
`1-77`: **LGTM: Well-structured test cassette for error handling**
The VCR cassette is correctly structured to test the invoice-not-found scenario. It properly simulates the API's 400 response with an appropriate error message when an invalid invoice ID is provided.
</details>
<details>
<summary>facturapi/resources/invoices.py (1)</summary>
`14-14`: **Correct usage of relative import**
Good job using relative import for internal modules, which aligns with the project's coding guidelines.
</details>
<details>
<summary>tests/resources/test_invoices.py (4)</summary>
`7-11`: **LGTM: Appropriate import of exception classes**
The proper exception classes are imported for the error handling test case.
---
`111-116`: **LGTM: Test for sending without email parameter**
Good test case covering the scenario where no email recipients are specified, which will use the customer's registered email.
---
`118-127`: **LGTM: Test for sending with email parameter**
Good test case for the scenario where specific email recipients are provided.
---
`136-143`: **LGTM: Test for invoice not found scenario**
Good test case covering error handling when an invoice ID doesn't exist. It properly verifies that the exception is raised and contains the expected error message.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`
</details>
<details>
<summary>`**/*.py`: ## MANDATORY: Use built-in Pydantic validators
### Description
Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in val...</summary>
> `**/*.py`: ## MANDATORY: Use built-in Pydantic validators
>
> ### Description
> Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in validators, pydantic_extra_types package, or third-party Pydantic validator libraries. This improves code maintainability and reduces unnecessary unit tests.
>
> ### Bad Practice
> ```python
> from pydantic import BaseModel, field_validator
>
> class MyValidator(BaseModel):
> location: str
>
> @field_validator('location')
> def validate_location(cls, value: str) -> str:
> values = value.split(',')
> if len(values) != 3:
> raise ValueError('Must provide exactly 3 values for location')
> # Custom validation logic that duplicates functionality
> return value
> ```
>
> ### Good Practice
> ```python
> from pydantic import BaseModel
> from pydantic_extra_types.coordinate import Coordinate
>
> class MyValidator(BaseModel):
> location: Coordinate
> ```
>
> ### Unit Test Guidelines
> Do not write unit tests specifically for validating the behavior of Pydantic's built-in validators. These are already well-tested by the Pydantic library itself.
>
> #### Tests to Remove
> ```python
> def test_invalid_location():
> pytest.raises(ValidationError):
> MyValidator(location='foo,bar')
> ```
>
> ### Rule Enforcement
> This is a mandatory rule, not a refactoring suggestion. Changes must be implemented when:
> 1. A custom validator replicates functionality already available in Pydantic's ecosystem
> 2. There is a suitable built-in, pydantic_extra_types, or third-party Pydantic validator available
>
> Actions required:
> 1. Replace custom validators with appropriate existing validators
> 2. Remove unnecessary unit tests that only validate built-in Pydantic validation behavior
> 3. Block PRs that introduce new custom validators when alternatives exist
- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (6)</summary><blockquote>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_invoice_not_found.yaml (1)</summary>
`1-77`: **LGTM: Well-structured test cassette for error handling**
The VCR cassette is correctly structured to test the invoice-not-found scenario. It properly simulates the API's 400 response with an appropriate error message when an invalid invoice ID is provided.
</details>
<details>
<summary>facturapi/resources/invoices.py (1)</summary>
`14-14`: **Correct usage of relative import**
Good job using relative import for internal modules, which aligns with the project's coding guidelines.
</details>
<details>
<summary>tests/resources/test_invoices.py (4)</summary>
`7-11`: **LGTM: Appropriate import of exception classes**
The proper exception classes are imported for the error handling test case.
---
`111-116`: **LGTM: Test for sending without email parameter**
Good test case covering the scenario where no email recipients are specified, which will use the customer's registered email.
---
`118-127`: **LGTM: Test for sending with email parameter**
Good test case for the scenario where specific email recipients are provided.
---
`136-143`: **LGTM: Test for invoice not found scenario**
Good test case covering error handling when an invoice ID doesn't exist. It properly verifies that the exception is raised and contains the expected error message.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
facturapi/resources/invoices.py (1)
195-226:⚠️ Potential issueMissing validation for invoice_id parameter
The docstring mentions raising
FacturapiResponseExceptionif the invoice_id is not found, but there's no validation to check ifinvoice_idis provided. This validation was suggested in a previous review comment but hasn't been implemented yet.Add validation for the
invoice_idparameter:@classmethod def send_by_email( cls, invoice_id: str, recipients: Optional[Union[str, List[str]]] = None, ) -> bool: """Send an invoice by email. Sends an email to the customer's email address with the XML and PDF files attached. Args: invoice_id: The ID of the invoice to send. recipients: The email addresses to send the invoice to. If not provided, the invoice will be sent to the customer's registered email. Returns: bool: True if the email was sent successfully, False otherwise. Raises: FacturapiResponseException: If the invoice_id is not found. requests.RequestException: If the API request fails. """ + if not invoice_id: + raise ValueError("invoice_id is required") endpoint = f"{cls._resource}/{invoice_id}/email" payload = {} if recipients: payload["email"] = recipients response = client.post(endpoint, payload) return response.get("ok", False)
🧹 Nitpick comments (1)
facturapi/resources/invoices.py (1)
215-218: Docstring-implementation mismatchThe docstring mentions raising a
ValueErrorifinvoice_idis not provided, but mentionsFacturapiResponseExceptionfor when the invoice is not found. This inconsistency should be fixed to align with the actual implementation.Update the docstring to be consistent with the implementation:
Raises: - FacturapiResponseException: If the invoice_id is not found. + ValueError: If the invoice_id is not provided. + FacturapiResponseException: If the invoice is not found. requests.RequestException: If the API request fails.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
facturapi/resources/invoices.py(2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
`**/*.py`: Enforce Relative Imports for Internal Modules
Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...
**/*.py: Enforce Relative Imports for Internal ModulesEnsure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.
Examples and Guidelines:
- If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
- If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
- If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
- External (third-party) libraries should be imported absolutely (e.g., import requests).
facturapi/resources/invoices.py
`**/*.py`:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic...
**/*.py:
Rule: Enforce Snake Case in Python Backend
- New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
- Exceptions (Pydantic models for API responses):
- Primary fields must be snake_case.
- If older clients expect camelCase, create a computed or alias field that references the snake_case field.
- Mark any camelCase fields as deprecated or transitional.
Examples
Invalid:
class CardConfiguration(BaseModel): title: str subTitle: str # ❌ Modified or new field in camelCaseValid:
class CardConfiguration(BaseModel): title: str subtitle: str # ✅ snake_case for new/modified field @computed_field def subTitle(self) -> str: # camelCase allowed only for compatibility return self.subtitleAny direct use of camelCase in new or updated code outside of these exceptions should be flagged.
facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: ## MANDATORY: Use built-in Pydantic validators
### Description
Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in val...</summary>
> `**/*.py`: ## MANDATORY: Use built-in Pydantic validators
>
> ### Description
> Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in validators, pydantic_extra_types package, or third-party Pydantic validator libraries. This improves code maintainability and reduces unnecessary unit tests.
>
> ### Bad Practice
> ```python
> from pydantic import BaseModel, field_validator
>
> class MyValidator(BaseModel):
> location: str
>
> @field_validator('location')
> def validate_location(cls, value: str) -> str:
> values = value.split(',')
> if len(values) != 3:
> raise ValueError('Must provide exactly 3 values for location')
> # Custom validation logic that duplicates functionality
> return value
> ```
>
> ### Good Practice
> ```python
> from pydantic import BaseModel
> from pydantic_extra_types.coordinate import Coordinate
>
> class MyValidator(BaseModel):
> location: Coordinate
> ```
>
> ### Unit Test Guidelines
> Do not write unit tests specifically for validating the behavior of Pydantic's built-in validators. These are already well-tested by the Pydantic library itself.
>
> #### Tests to Remove
> ```python
> def test_invalid_location():
> pytest.raises(ValidationError):
> MyValidator(location='foo,bar')
> ```
>
> ### Rule Enforcement
> This is a mandatory rule, not a refactoring suggestion. Changes must be implemented when:
> 1. A custom validator replicates functionality already available in Pydantic's ecosystem
> 2. There is a suitable built-in, pydantic_extra_types, or third-party Pydantic validator available
>
> Actions required:
> 1. Replace custom validators with appropriate existing validators
> 2. Remove unnecessary unit tests that only validate built-in Pydantic validation behavior
> 3. Block PRs that introduce new custom validators when alternatives exist
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (2)</summary><blockquote>
<details>
<summary>facturapi/resources/invoices.py (2)</summary>
`14-14`: **LGTM! Correct use of relative imports**
The change to use relative imports for internal modules follows the coding guidelines correctly.
---
`220-225`: **LGTM! Clean and concise implementation**
The implementation is clean and follows the direct approach suggested in previous reviews. It handles both single email addresses and lists of addresses, and returns a boolean value as recommended.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
try:
can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>
> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
>
> ```python
> try:
> can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
> can_ignore_error = False
> ```
>
> ❌ Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
> 'code' in data['error']
> and data['error']['code'] in ignore_error_codes
> )
> ```
>
> Explanation:
> The try/except approach:
>
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
>
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
Rule
Always annotate @t...</summary>
> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
>
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
>
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
> # The message is already a User instance - no conversion needed
> print(message.name) # Direct attribute access
> ```
>
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
>
> class User(BaseModel):
> name: str
> age: int
>
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None: # or unannotated parameter
> # Unnecessary conversion
> message = User(**message_data)
> print(message.name)
> ```
>
> Explanation
> The Agave @task decorator automatically:
>
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model
- `facturapi/resources/invoices.py`
</details>
<details>
<summary>`**/*.py`: ## MANDATORY: Use built-in Pydantic validators
### Description
Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in val...</summary>
> `**/*.py`: ## MANDATORY: Use built-in Pydantic validators
>
> ### Description
> Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in validators, pydantic_extra_types package, or third-party Pydantic validator libraries. This improves code maintainability and reduces unnecessary unit tests.
>
> ### Bad Practice
> ```python
> from pydantic import BaseModel, field_validator
>
> class MyValidator(BaseModel):
> location: str
>
> @field_validator('location')
> def validate_location(cls, value: str) -> str:
> values = value.split(',')
> if len(values) != 3:
> raise ValueError('Must provide exactly 3 values for location')
> # Custom validation logic that duplicates functionality
> return value
> ```
>
> ### Good Practice
> ```python
> from pydantic import BaseModel
> from pydantic_extra_types.coordinate import Coordinate
>
> class MyValidator(BaseModel):
> location: Coordinate
> ```
>
> ### Unit Test Guidelines
> Do not write unit tests specifically for validating the behavior of Pydantic's built-in validators. These are already well-tested by the Pydantic library itself.
>
> #### Tests to Remove
> ```python
> def test_invalid_location():
> pytest.raises(ValidationError):
> MyValidator(location='foo,bar')
> ```
>
> ### Rule Enforcement
> This is a mandatory rule, not a refactoring suggestion. Changes must be implemented when:
> 1. A custom validator replicates functionality already available in Pydantic's ecosystem
> 2. There is a suitable built-in, pydantic_extra_types, or third-party Pydantic validator available
>
> Actions required:
> 1. Replace custom validators with appropriate existing validators
> 2. Remove unnecessary unit tests that only validate built-in Pydantic validation behavior
> 3. Block PRs that introduce new custom validators when alternatives exist
- `facturapi/resources/invoices.py`
</details>
</details><details>
<summary>🧬 Code Definitions (1)</summary>
<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>
<details>
<summary>facturapi/http/client.py (2)</summary>
* `post` (67-69)
* `get` (59-65)
</details>
</blockquote></details>
</details>
</details>
<details>
<summary>🔇 Additional comments (2)</summary><blockquote>
<details>
<summary>facturapi/resources/invoices.py (2)</summary>
`14-14`: **LGTM! Correct use of relative imports**
The change to use relative imports for internal modules follows the coding guidelines correctly.
---
`220-225`: **LGTM! Clean and concise implementation**
The implementation is clean and follows the direct approach suggested in previous reviews. It handles both single email addresses and lists of addresses, and returns a boolean value as recommended.
</details>
</blockquote></details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->|
@felipao-mx @rogelioLpz podemos mezclar el PR de este ser de luz? |


Summary
Feature: Send Invoices via Email
This PR adds functionality to send invoices via email in the
facturapiPython client.Changes
Core Implementation:
send_by_emailmethod inInvoiceclass:/v2/invoices/{invoice_id}/email).ValueErrorifinvoice_idis missing.Version Update:
facturapi/version.pyfrom0.1.1→0.2.0.Tests:
test_send_invoice_by_email_with_email.yaml):test_invoices.py:test_send_invoice_by_email_with_email(): Validates successful email sending.test_send_invoice_by_email_missing_invoice_id(): Ensures validation error is raised.Review Notes
Summary by CodeRabbit
New Features
Tests
Chores
Summary by CodeRabbit
Summary by CodeRabbit