Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds organisation API support #30

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

yarcod
Copy link

@yarcod yarcod commented Dec 10, 2024

Apart from operating a single charge station, the Charge Amps API also allows controlling (part) of an organisation's fleet of charge stations and users. Since this is most likely an uncommon usecase, I placed it behind a feature flag "organisations".

This does not add support for CLI flags for accessing the API, but would be a nice future addition.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a "How to use" section in the README with installation instructions for the Charge Amps Python module.
    • Introduced the OrganisationClient class for managing organization-related API interactions.
    • Added multiple asynchronous methods in OrganisationClient for retrieving and manipulating organization data.
    • Implemented new classes and methods for enhanced user and partner data management.
  • Bug Fixes

    • Improved handling of user data during token refresh and login processes.
  • Documentation

    • Updated README to clarify installation methods and usage.
    • Introduced a new testing script for the OrganisationClient.
  • Chores

    • Changed configuration file name from credentials.json to config.json in the testing scripts.

Apart from operating a single charge station, the Charge Amps API also
allows controlling (part) of an organization's fleet of charge stations
and users. Since this is most likely an uncommon usecase, I placed it
behind a feature flag "organisations".
This does not add support for CLI flags for accessing the API, but would
be a nice future addition.
@jschlyter jschlyter self-requested a review December 11, 2024 05:59
@jschlyter
Copy link
Member

@coderabbitai review

Copy link

coderabbitai bot commented Dec 11, 2024

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Dec 11, 2024

Walkthrough

The changes in this pull request introduce several enhancements to the Charge Amps Python module. Notably, a new section in the README.md provides installation instructions. The ChargeAmpsExternalClient class in external.py has been updated with new user and partner data handling methods. New classes and a feature flag decorator have been added in models.py. A new OrganisationClient class has been introduced in organisation.py to manage organization-related API interactions. Additionally, two new testing scripts have been created to facilitate testing of the new functionalities.

Changes

File Change Summary
README.md Added "How to use" section with installation instructions for the Charge Amps Python module.
chargeamps/external.py Updated ChargeAmpsExternalClient class: added _user variable, modified _ensure_token, and added methods get_partner and get_logged_in_user.
chargeamps/models.py Introduced feature_required decorator and added new classes: RfidTag, User, Partner, Rfid, Organisation, OrganisationChargingSession.
chargeamps/organisation.py Added OrganisationClient class with multiple methods for managing organization data and interactions with the API.
examples/org_test.py Introduced asynchronous testing script for OrganisationClient with functionality to test various API interactions.
examples/test.py Modified configuration file name from credentials.json to config.json in the test function.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant OrganisationClient
    participant API

    User->>OrganisationClient: Request to get organisations
    OrganisationClient->>API: GET /organisations
    API-->>OrganisationClient: Return list of organisations
    OrganisationClient-->>User: Return organisations list
Loading

🐰 "In the code, a new path we pave,
With users and partners, their data we save.
From README to tests, our features now shine,
In Charge Amps' world, everything's fine!
So hop along, dear coder, don't delay,
Embrace the changes, let’s code and play!" 🐇✨

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (6)
chargeamps/organisation.py (3)

53-56: Simplify list constructions using list comprehensions

In several methods, you are building lists using loops where list comprehensions can enhance readability and conciseness.

For example, in get_organisation_chargepoints:

     payload = await response.json()

-    res = []
-    for charge_point in payload:
-        res.append(ChargePoint.model_validate(charge_point))
-    return res
+    return [ChargePoint.model_validate(cp) for cp in payload]

Apply similar changes in the following methods:

  • get_organisation_chargepoint_statuses (lines 66-69)
  • get_organisation_rfids (lines 140-143)
  • get_organisation_users (lines 186-189)

Also applies to: 66-69, 140-143, 186-189


222-222: Replace print statement with proper logging

Using print statements in production code is not recommended. Use the logging framework to log debug information.

-    print(payload)
+    self._logger.debug(f"Payload: {payload}")

215-220: Align password length validation condition and error message

The condition checks if len(password) >= 8, but the error message states "must exceed 8 characters," implying len(password) > 8. To avoid confusion, align the condition with the error message.

Option 1: Change the condition to match the error message:

-    if len(password) >= 8:
+    if len(password) > 8:

Option 2: Update the error message to match the condition:

     else:
         raise ValueError(
-            "The provided password is too short, must exceed 8 characters"
+            "The provided password is too short, must be at least 8 characters"
         )
README.md (1)

11-13: Specify the language identifier in fenced code blocks

The fenced code blocks on lines 11-13 and 18-19 do not specify a language, which affects syntax highlighting and readability. This issue is flagged by Markdownlint.

Add the language identifier (e.g., bash) to the code blocks:

 ```bash
 pip install chargeamps

```diff
 ```bash
 pip install chargeamps["organisations"]



Also applies to: 18-19

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Markdownlint (0.35.0)</summary>

11-11: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

</blockquote></details>
<details>
<summary>chargeamps/external.py (2)</summary><blockquote>

`49-49`: **Consider thread-safety implications of the _user field**

The `_user` field maintains state that could lead to race conditions in concurrent usage scenarios. Consider making this field immutable or implementing proper synchronization.

---

`78-78`: **Ensure consistent state management across token operations**

The `_user` field is managed across multiple locations. Consider centralizing this logic to prevent inconsistencies.

Consider creating a dedicated method for user state management:

```python
def _clear_auth_state(self):
    """Clear all authentication-related state."""
    self._token = None
    self._user = None
    self._refresh_token = None
    self._token_expire = 0

def _update_auth_state(self, response_payload: dict):
    """Update authentication state from response payload."""
    self._token = response_payload["token"]
    self._user = response_payload["user"]
    self._refresh_token = response_payload["refreshToken"]
    # Update token expiration...

Also applies to: 96-96, 108-108

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c1e1a9b and e6b71b8.

📒 Files selected for processing (6)
  • README.md (1 hunks)
  • chargeamps/external.py (7 hunks)
  • chargeamps/models.py (2 hunks)
  • chargeamps/organisation.py (1 hunks)
  • examples/org_test.py (1 hunks)
  • examples/test.py (1 hunks)
🧰 Additional context used
🪛 Markdownlint (0.35.0)
README.md

11-11: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


17-17: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (2)
chargeamps/organisation.py (1)

123-125: ⚠️ Potential issue

Fix incorrect usage of await payload.json() in loop

In the get_organisation_charging_sessions method, you're calling await payload.json() inside the loop, but payload is already a JSON-decoded object. This will raise an error.

Update the loop to iterate directly over payload:

-    for session in await payload.json():
+    for session in payload:

Likely invalid or redundant comment.

examples/test.py (1)

8-8: Change looks good

The update to read from "config.json" instead of "credentials.json" ensures consistency with other example scripts.

Comment on lines 18 to 25
def feature_required(feature_flag):
def decorator(cls):
if feature_flag:
return cls
else:
raise ImportError(f"Feature '{feature_flag}' is not enabled")

return decorator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the feature_required decorator to correctly enforce feature flags

The current implementation of the feature_required decorator checks if feature_flag is truthy, but since feature_flag is a non-empty string (e.g., "organisations"), this condition will always be true. As a result, the feature gating doesn't function as intended. Instead, the decorator should check whether the feature is enabled in the application settings or configuration.

Consider modifying the decorator to check a global settings object or configuration that maintains the status of feature flags. For example:

 def feature_required(feature_flag):
     def decorator(cls):
-        if feature_flag:
+        if settings.FEATURE_FLAGS.get(feature_flag, False):
             return cls
         else:
             raise ImportError(f"Feature '{feature_flag}' is not enabled")
     return decorator

Ensure that you import the settings module or wherever the feature flags are defined.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 165 to 170
async def revoke_organisation_rfid(self, org_id: str, rfid: Rfid) -> None:
"""Revoke an RFID tag"""
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/rfids/revoke"
rfid_id = rfid.rfid
payload = rfid_id.toJson()
await self._put(request_uri, json=payload)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect use of toJson() on rfid_id in revoke_organisation_rfid

The variable rfid_id is a string, and strings do not have a toJson() method. This will raise an AttributeError.

Modify the code to construct the payload correctly:

     rfid_id = rfid.rfid
-    payload = rfid_id.toJson()
+    payload = {"rfid": rfid_id}
     await self._put(request_uri, json=payload)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def revoke_organisation_rfid(self, org_id: str, rfid: Rfid) -> None:
"""Revoke an RFID tag"""
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/rfids/revoke"
rfid_id = rfid.rfid
payload = rfid_id.toJson()
await self._put(request_uri, json=payload)
async def revoke_organisation_rfid(self, org_id: str, rfid: Rfid) -> None:
"""Revoke an RFID tag"""
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/rfids/revoke"
rfid_id = rfid.rfid
payload = {"rfid": rfid_id}
await self._put(request_uri, json=payload)

Comment on lines 198 to 214
rfid: [Rfid] = None,
password: str = None,
) -> User:
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/users"
payload = {}

if first_name:
payload["firstName"] = first_name
if last_name:
payload["lastName"] = last_name
if email:
payload["email"] = email
if mobile:
payload["mobile"] = mobile
if rfid:
payload["rfidTags"] = rfid.model_dump(by_alias=True)
if password:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct handling of rfid parameter in add_organisation_user

The parameter rfid is annotated as [Rfid], which is invalid syntax. Additionally, when adding it to the payload, you're calling model_dump on a list, which will cause an error.

Update the type annotation and adjust the payload construction:

-    rfid: [Rfid] = None,
+    rfid: Optional[list[Rfid]] = None,
...
     if rfid:
-        payload["rfidTags"] = rfid.model_dump(by_alias=True)
+        payload["rfidTags"] = [tag.model_dump(by_alias=True) for tag in rfid]

Ensure you import Optional and list from the typing module.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rfid: [Rfid] = None,
password: str = None,
) -> User:
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/users"
payload = {}
if first_name:
payload["firstName"] = first_name
if last_name:
payload["lastName"] = last_name
if email:
payload["email"] = email
if mobile:
payload["mobile"] = mobile
if rfid:
payload["rfidTags"] = rfid.model_dump(by_alias=True)
if password:
rfid: Optional[list[Rfid]] = None,
password: str = None,
) -> User:
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/users"
payload = {}
if first_name:
payload["firstName"] = first_name
if last_name:
payload["lastName"] = last_name
if email:
payload["email"] = email
if mobile:
payload["mobile"] = mobile
if rfid:
payload["rfidTags"] = [tag.model_dump(by_alias=True) for tag in rfid]
if password:

Comment on lines 71 to 95
def verify_rfid(
self, rfid: str, rfid_format: str, rfid_length: str, rfid_dec_format_length: str
) -> dict[str, str]:
result = {}
result["rfid"] = rfid
if rfid_format == "Hex":
# TODO: Should actual `rfid` param be verified against the length here?
if not rfid_length:
return result
elif rfid_format == "Dec" or rfid_format == "ReverseDec":
if rfid_dec_format_length:
result["rfidDecimalFormatLength"] = rfid_dec_format_length
else:
self._logger.error("Invalid RFID format")
return {}

if rfid_length in {4, 7, 10}:
result["rfidLength"] = rfid_length
else:
self._logger.error("RFID length invalid, should be either 4, 7 or 10 bytes")
return {}

result["rfidFormat"] = rfid_format
return result

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct parameter types and logic in verify_rfid method

The parameters rfid_length and rfid_dec_format_length are used as integers in comparisons, but are typed as str in the method signature. This can lead to type errors during runtime. Additionally, there's a TODO comment regarding RFID length verification.

Update the parameter types to int and implement the RFID length validation. Here's the corrected method signature and adjustments:

 def verify_rfid(
-        self, rfid: str, rfid_format: str, rfid_length: str, rfid_dec_format_length: str
+        self, rfid: str, rfid_format: str, rfid_length: int = None, rfid_dec_format_length: int = None
    ) -> dict[str, str]:

Adjust the logic within the method to handle None values and perform appropriate integer comparisons.

Would you like assistance in implementing the RFID length validation logic or updating the method accordingly?

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 145 to 154
async def add_organisation_rfid(
self, org_id: str, rfid: Rfid, rfid_dec_format_length: int = None
) -> RfidTag:
"""Add a new RFID tag to the organisation"""
request_uri = f"/api/{API_VERSION}/organisations/{org_id}/rfids"
payload = rfid.model_dump(by_alias=True)
if rfid_dec_format_length:
payload["rfidDecimalFormatLength"] = rfid_dec_format_length
await self._put(request_uri, json=payload)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle missing return value in add_organisation_rfid

The method add_organisation_rfid is expected to return an RfidTag object, but currently, it doesn't return anything.

Modify the method to process the response and return the new RfidTag:

     await self._put(request_uri, json=payload)
+    response = await self._put(request_uri, json=payload)
+    payload = await response.json()
+    return RfidTag.model_validate(payload)

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 32 to 35
if __name__ == "__main__":
# asyncio.run(test())
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use asyncio.run() instead of manual event loop handling

The manual event loop handling is deprecated. Use asyncio.run() for better maintainability.

-    loop = asyncio.get_event_loop()
-    loop.run_until_complete(test())
+    asyncio.run(test())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if __name__ == "__main__":
# asyncio.run(test())
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
if __name__ == "__main__":
# asyncio.run(test())
asyncio.run(test())

Comment on lines +14 to +28
chargepoints = await client.get_organisation_chargepoints()
print(chargepoints)

status = await client.get_chargepoint_status(chargepoints[0].id)
print("Status:", status)

for c in status.connector_statuses:
settings = await client.get_chargepoint_connector_settings(
c.charge_point_id, c.connector_id
)
print("Before:", settings)
settings.max_current = 11
await client.set_chargepoint_connector_settings(settings)
print("After:", settings)

Copy link

@coderabbitai coderabbitai bot Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for API operations

The script performs critical operations (fetching status, modifying settings) without proper error handling. This could lead to partial updates or silent failures.

Example implementation:

+async def modify_connector_settings(client, chargepoint_id, connector_id, new_current):
+    try:
+        settings = await client.get_chargepoint_connector_settings(
+            chargepoint_id, connector_id
+        )
+        print(f"Current settings: {settings}")
+        
+        settings.max_current = new_current
+        await client.set_chargepoint_connector_settings(settings)
+        print(f"Settings updated successfully: {settings}")
+    except Exception as e:
+        print(f"Failed to update settings: {e}")
+        raise

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, example is not meant to be run for more than just trying out the module. It is kept similar to the existing example

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yarcod, understood. I'll note that the example scripts in the examples/ directory are intended for trying out the module and are kept similar to existing examples.


✏️ Learnings added
Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: examples/org_test.py:14-28
Timestamp: 2024-12-17T22:35:19.341Z
Learning: In the `examples/` directory, Python scripts like `org_test.py` are intended primarily for trying out the module and are kept similar to existing examples, and may not include comprehensive error handling.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

c.charge_point_id, c.connector_id
)
print("Before:", settings)
settings.max_current = 11
Copy link

@coderabbitai coderabbitai bot Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

⚠️ Potential issue

Hardcoded value is inconsistent with configurable design

The codebase already implements a configurable approach for max_current through:

  • CLI argument --current in chargeamps/cli.py
  • Optional field in models.py (max_current: Optional[float])
  • Different values used across test files (11A in org_test.py, 6A in test.py)

The hardcoded value of 11 in org_test.py should follow the same configurable pattern used elsewhere in the codebase.

🔗 Analysis chain

Avoid hardcoding configuration values

The max_current value of 11 is hardcoded. Consider making this configurable to prevent accidental modifications.


Let me check if this value is defined elsewhere or if there's a configuration mechanism in place.


Let me check the configuration file and CLI argument handling to understand how max_current is typically configured.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if this value is used consistently across the codebase
rg "max_current\s*=\s*11" --type py

Length of output: 92


Script:

#!/bin/bash
# Search for max_current usage and definition patterns
rg "max_current" --type py -A 2

# Look for potential configuration files
fd -e yaml -e yml -e json -e ini -e conf -e cfg -e toml

# Check if there's a settings class definition
ast-grep --pattern 'class $_ {
  $$$
  max_current
  $$$
}'

Length of output: 1065


Script:

#!/bin/bash
# Check the config file content
cat examples/config.json

# Look at the CLI argument parser setup
rg -B 5 "dest=\"max_current\"" chargeamps/cli.py

Length of output: 374

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why just this function needed more detailed documentation, but similarly to above I will keep both the docstring and error handling consistent with the rest of the module.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yarcod, understood. Thank you for keeping both the docstring and error handling consistent with the rest of the module.


✏️ Learnings added
Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: examples/org_test.py:25-25
Timestamp: 2024-12-17T22:35:45.702Z
Learning: In the `python-chargeamps` project, when suggesting improvements to docstrings or error handling, ensure recommendations are consistent with existing coding practices in the codebase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 243 to 251
async def get_logged_in_user(self) -> User:
"""Get authenticated user info"""
user_id = self._user["id"]

request_uri = f"/api/{API_VERSION}/users/{user_id}"
response = await self._get(request_uri)
payload = await response.json()

return User.model_validate(payload)
Copy link

@coderabbitai coderabbitai bot Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling and type safety in get_logged_in_user

The method assumes self._user is a dict and contains an 'id' key without validation.

     async def get_logged_in_user(self) -> User:
         """Get authenticated user info"""
+        if not self._user or not isinstance(self._user, dict):
+            raise ValueError("No user is currently logged in")
+        
+        user_id = self._user.get("id")
+        if not user_id:
+            raise ValueError("Invalid user data: missing id")
-        user_id = self._user["id"]
 
         request_uri = f"/api/{API_VERSION}/users/{user_id}"
-        response = await self._get(request_uri)
-        payload = await response.json()
-
-        return User.model_validate(payload)
+        try:
+            response = await self._get(request_uri)
+            payload = await response.json()
+            return User.model_validate(payload)
+        except Exception as e:
+            self._logger.error(f"Failed to get user info: {e}")
+            raise

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the user class variable is of type User, and id of that class is not optional, there will always be an id there as long as a user has been assigned. Will add a check for if there is currently a user logged in though.
Regarding the error handling, this follows the same design pattern as elsewhere in the module. While good to have, I think it is better to keep the design consistent across the module and instead add error handling separately at a later stage if deemed necessary.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yarcod, understood. Thank you for clarifying that self._user is of type User and always contains an id when a user is assigned. Adding a check for whether a user is currently logged in makes sense.

Regarding error handling, I acknowledge your point about maintaining design consistency across the module. Implementing error handling uniformly in a future update is a reasonable approach.


✏️ Learnings added
Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: chargeamps/external.py:243-251
Timestamp: 2024-12-17T22:36:01.618Z
Learning: In `chargeamps/external.py`, `self._user` is of type `User`, and `id` is not optional; thus, `self._user["id"]` will always exist when a user is assigned.

Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: chargeamps/external.py:243-251
Timestamp: 2024-12-17T22:36:01.618Z
Learning: In `chargeamps/external.py`, error handling follows a consistent design pattern across the module; additional error handling should be added separately to maintain consistency.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +218 to +223
async def get_partner(self, charge_point_id: str) -> Partner:
"""Get partner details"""
request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/partner"
response = await self._get(request_uri)
payload = await response.json()
return Partner.model_validate(payload)
Copy link

@coderabbitai coderabbitai bot Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and documentation for get_partner method

The method needs better error handling and documentation of possible exceptions.

     async def get_partner(self, charge_point_id: str) -> Partner:
-        """Get partner details"""
+        """Get partner details for a specific charge point.
+        
+        Args:
+            charge_point_id: The ID of the charge point
+            
+        Returns:
+            Partner: The partner details
+            
+        Raises:
+            HTTPException: If the API request fails
+            ValidationError: If the response data is invalid
+        """
         request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/partner"
-        response = await self._get(request_uri)
-        payload = await response.json()
-        return Partner.model_validate(payload)
+        try:
+            response = await self._get(request_uri)
+            payload = await response.json()
+            return Partner.model_validate(payload)
+        except Exception as e:
+            self._logger.error(f"Failed to get partner details: {e}")
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_partner(self, charge_point_id: str) -> Partner:
"""Get partner details"""
request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/partner"
response = await self._get(request_uri)
payload = await response.json()
return Partner.model_validate(payload)
async def get_partner(self, charge_point_id: str) -> Partner:
"""Get partner details for a specific charge point.
Args:
charge_point_id: The ID of the charge point
Returns:
Partner: The partner details
Raises:
HTTPException: If the API request fails
ValidationError: If the response data is invalid
"""
request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/partner"
try:
response = await self._get(request_uri)
payload = await response.json()
return Partner.model_validate(payload)
except Exception as e:
self._logger.error(f"Failed to get partner details: {e}")
raise

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why just this function needed more detailed documentation, but similarly to above I will keep both the docstring and error handling consistent with the rest of the module.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yarcod, understood. Maintaining consistency in docstrings and error handling across the module makes sense. Thank you for clarifying.


✏️ Learnings added
Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: chargeamps/external.py:218-223
Timestamp: 2024-12-17T22:36:20.401Z
Learning: In `chargeamps/external.py`, ensure that docstrings and error handling remain consistent with the rest of the module, avoiding the addition of detailed documentation or extensive error handling to individual functions unless it's applied uniformly.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@jschlyter jschlyter marked this pull request as draft December 17, 2024 06:22
@jschlyter
Copy link
Member

jschlyter commented Dec 17, 2024

Please fix issues and mark as ready for review

@yarcod yarcod marked this pull request as ready for review December 17, 2024 22:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
chargeamps/organisation.py (1)

99-99: Remove unnecessary f-string prefix

The f-string on line 99 does not contain any placeholders, so the f prefix is unnecessary and can be removed.

Apply this diff to fix the issue:

-        raise ValueError(f"RFID length must be 7 bytes if the (default) format type 'Hex' is set.")
+        raise ValueError("RFID length must be 7 bytes if the (default) format type 'Hex' is set.")
🧰 Tools
🪛 Ruff (0.8.2)

99-99: f-string without any placeholders

Remove extraneous f prefix

(F541)

chargeamps/external.py (1)

243-254: Simplify get_logged_in_user by returning cached user data

Since self._user now holds a validated User instance, you can simplify the get_logged_in_user method by returning the cached user data without making an additional API call.

Apply this diff to streamline the method:

 async def get_logged_in_user(self) -> User:
     """Get authenticated user info"""
     if not self._user:
         raise ValueError("No user is currently logged in")
-    user_id = self._user.id
-    request_uri = f"/api/{API_VERSION}/users/{user_id}"
-    response = await self._get(request_uri)
-    payload = await response.json()
-    return User.model_validate(payload)
+    return self._user
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6b71b8 and f11dda2.

📒 Files selected for processing (4)
  • chargeamps/external.py (7 hunks)
  • chargeamps/models.py (2 hunks)
  • chargeamps/organisation.py (1 hunks)
  • examples/org_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/org_test.py
🧰 Additional context used
🪛 Ruff (0.8.2)
chargeamps/organisation.py

99-99: f-string without any placeholders

Remove extraneous f prefix

(F541)

🔇 Additional comments (2)
chargeamps/organisation.py (1)

84-106: Correct parameter types in verify_rfid method

The parameters rfid_length and rfid_dec_format_length are typed as str | None, but they are used as integers in comparisons. This can lead to type errors at runtime. Update the parameter types to int | None for consistency.

🧰 Tools
🪛 Ruff (0.8.2)

99-99: f-string without any placeholders

Remove extraneous f prefix

(F541)

chargeamps/models.py (1)

18-24: Fix the feature_required decorator to correctly enforce feature flags

The current implementation of the feature_required decorator only allows the "organisations" feature, regardless of actual feature flag settings. This does not correctly enforce feature flags as intended.

Consider modifying the decorator to check a configuration or settings object that maintains the status of feature flags. For example:

 def feature_required(feature_flag):
     def decorator(cls):
-        if feature_flag == "organisations":
+        if settings.FEATURE_FLAGS.get(feature_flag, False):
             return cls
         else:
             raise ImportError(f"Feature '{feature_flag}' is not enabled")
     return decorator

Ensure you import the settings module or define where FEATURE_FLAGS are stored.

Comment on lines +250 to +251
"""Get organisation's registered users"""
query_params = {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Pass query parameters in get_organisation_user

The query_params dictionary is constructed but not passed to the _get method. As a result, any query parameters are not being included in the request.

Apply this diff to fix the issue:

-            response = await self._get(request_uri)
+            response = await self._get(request_uri, params=query_params)

Committable suggestion skipped: line range outside the PR's diff.

@@ -100,6 +105,7 @@ async def _ensure_token(self) -> None:
response_payload = await response.json()

self._token = response_payload["token"]
self._user = response_payload["user"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent typing of self._user

The instance variable self._user is annotated as User but is assigned a dictionary from response_payload["user"]. This can lead to type inconsistencies and potential errors.

Apply this diff to validate and assign the user data correctly:

-            self._user = response_payload["user"]
+            self._user = User.model_validate(response_payload["user"])

This ensures self._user is always a User instance.

Additionally, in the get_logged_in_user method, you can return self._user directly:

-        user_id = self._user["id"]
-        request_uri = f"/api/{API_VERSION}/users/{user_id}"
-        response = await self._get(request_uri)
-        payload = await response.json()
-        return User.model_validate(payload)
+        return self._user

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants