Skip to content

Scheduler endpoints#13

Merged
hugobessa merged 2 commits intomainfrom
feat/scheduler-endpoints
Oct 10, 2025
Merged

Scheduler endpoints#13
hugobessa merged 2 commits intomainfrom
feat/scheduler-endpoints

Conversation

@hugobessa
Copy link
Copy Markdown
Contributor

@hugobessa hugobessa commented Oct 10, 2025

Summary by Sourcery

Introduce token-based public scheduler endpoints for calendar event management with serializer, service, and permission layer enhancements, update routing and schema, and add integration tests

New Features:

  • Add public token-based endpoints for calendar events (list, create, retrieve, update, partial update, delete) under /public/organizations/{organization_id}/events
  • Implement TokenAuthenticationMixin and TokenCalendarEventViewSet to handle calendar management tokens in controller layer
  • Extend CalendarEventSerializer to support token context and differentiate between user-based and token-based authentication flows
  • Enhance CalendarService and CalendarPermissionService to initialize with management tokens and enforce permission checks

Enhancements:

  • Improve token parsing and error handling in CalendarPermissionService
  • Refactor CalendarService to unify token and user authentication initialization
  • Update API routing to include public token-based endpoints and adjust docker-compose volume configuration

Documentation:

  • Update OpenAPI schema to document new public token-based scheduler endpoints

Tests:

  • Add comprehensive integration tests for token-based calendar event operations covering create, update, delete, and permission scenarios

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Oct 10, 2025

Reviewer's Guide

This PR adds token-based authentication to the calendar event API by extending serializers and services to handle management tokens alongside regular user authentication, defines new public endpoints in the OpenAPI schema, and provides a dedicated token-driven viewset with comprehensive integration tests.

Sequence diagram for token-based event creation via public endpoint

sequenceDiagram
    actor Client
    participant API["/public/organizations/{organization_id}/events/"]
    participant ViewSet["TokenCalendarEventViewSet"]
    participant CalendarService
    participant CalendarPermissionService
    participant DB["CalendarEvent DB"]
    Client->>API: POST /public/organizations/{organization_id}/events/ (Bearer token)
    API->>ViewSet: create(request)
    ViewSet->>CalendarService: initialize_without_provider(token, organization)
    CalendarService->>CalendarPermissionService: initialize_with_token(token, organization_id)
    CalendarPermissionService-->>CalendarService: permission validated
    ViewSet->>DB: create CalendarEvent
    DB-->>ViewSet: CalendarEvent created
    ViewSet-->>API: return created event
    API-->>Client: 201 Created
Loading

Sequence diagram for token-based event update via public endpoint

sequenceDiagram
    actor Client
    participant API["/public/organizations/{organization_id}/events/{id}/"]
    participant ViewSet["TokenCalendarEventViewSet"]
    participant CalendarService
    participant CalendarPermissionService
    participant DB["CalendarEvent DB"]
    Client->>API: PUT/PATCH /public/organizations/{organization_id}/events/{id}/ (Bearer token)
    API->>ViewSet: update/partial_update(request)
    ViewSet->>CalendarService: initialize_without_provider(token, organization)
    CalendarService->>CalendarPermissionService: initialize_with_token(token, organization_id)
    CalendarPermissionService-->>CalendarService: permission validated
    ViewSet->>DB: update CalendarEvent
    DB-->>ViewSet: CalendarEvent updated
    ViewSet-->>API: return updated event
    API-->>Client: 200 OK
Loading

Class diagram for token-based calendar event viewset and mixin

classDiagram
    class TokenAuthenticationMixin {
        +calendar_service: CalendarService | None
        +extract_token_from_header(request: Request) tuple[str, int]
        +get_calendar_service(request: Request, organization) CalendarService
    }
    class TokenCalendarEventViewSet {
        +serializer_class: CalendarEventSerializer
        +queryset: CalendarEvent.objects.all()
        +authentication_classes: tuple
        +permission_classes: tuple
        +get_serializer_context()
        +get_queryset()
        +create()
        +update()
        +partial_update()
        +destroy()
        +list()
        +retrieve()
        +perform_create(serializer)
        +perform_update(serializer)
        +perform_destroy(instance)
    }
    TokenCalendarEventViewSet --|> TokenAuthenticationMixin
    TokenCalendarEventViewSet --|> NoListVintaScheduleModelViewSet
    TokenCalendarEventViewSet o-- CalendarEventSerializer
    TokenCalendarEventViewSet o-- CalendarService
Loading

File-Level Changes

Change Details Files
Enhanced CalendarEventSerializer to support token context and unify auth flows
  • Introduced user, token, token_str_base64, and organization attributes in init
  • Refactored nested serializer querysets to use computed organization_id and user_is_authenticated
  • Adjusted validate_provider, validate, create, and update methods to branch on token vs user auth and infer calendar context
calendar_integration/serializers.py
Added token-based public endpoints to OpenAPI schema
  • Defined GET/POST/PUT/PATCH/DELETE under /public/organizations/{organization_id}/events/
  • Referenced CalendarEvent and PaginatedCalendarEventList schemas for token routes
schema.yml
Hardened CalendarPermissionService token parsing and scoping
  • Wrapped base64 decoding and splitting in try/except, raising InvalidTokenError
  • Added organization_id filter before .get() calls
  • Standardized exception messages and chaining
calendar_integration/services/calendar_permission_service.py
Refactored CalendarService token initialization logic
  • Invoke initialize_with_token in initialize_without_provider when using a token
  • Removed duplicate token/auth checks in create_event, update_event, and delete_event methods
calendar_integration/services/calendar_service.py
Introduced WriteOnlyVintaScheduleModelViewSet for read/create/delete only
  • Added new viewset class inheriting List, Create, Update, Destroy mixins
  • Left implementation empty to enforce no-update semantics
common/utils/view_utils.py
Implemented token-driven viewset and routing
  • Created TokenAuthenticationMixin and TokenCalendarEventViewSet with manual auth
  • Injected CalendarService and overridden get_serializer_context and CRUD methods
  • Registered token routes in token_urls.py and included in main urls.py
calendar_integration/token_views.py
calendar_integration/token_urls.py
vinta_schedule_api/urls.py
Added integration tests for token-based calendar event endpoints
  • Wrote pytest-based tests covering create, update, retrieve, delete flows with various token types
  • Tested error cases: missing, invalid, unauthorized, revoked, and malformed tokens
calendar_integration/tests/test_token_views.py
Miscellaneous cleanup: exception, Docker, and URLs
  • Made CalendarPermissionError subclass PermissionDenied
  • Removed delegated volume flag in docker-compose.yml
  • Ensured token_urls are mounted under /public/ path
calendar_integration/exceptions.py
docker-compose.yml
vinta_schedule_api/urls.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

Blocking issues:

  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)

General comments:

  • The CalendarEventSerializer init has grown very large and mixes multiple responsibilities—consider extracting token/user/org context setup and queryset configuration into smaller helper methods or a dedicated context manager for readability and maintainability.
  • The TokenCalendarEventViewSet repeats the same token extraction and permission check logic in each action—extract this into a decorator or shared base method to DRY up the code and reduce boilerplate.
  • The WriteOnlyVintaScheduleModelViewSet class description says it doesn’t allow updates, yet it includes UpdateModelMixin—either update the documentation or remove the conflicting mixin to align behavior and docs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The CalendarEventSerializer __init__ has grown very large and mixes multiple responsibilities—consider extracting token/user/org context setup and queryset configuration into smaller helper methods or a dedicated context manager for readability and maintainability.
- The TokenCalendarEventViewSet repeats the same token extraction and permission check logic in each action—extract this into a decorator or shared base method to DRY up the code and reduce boilerplate.
- The WriteOnlyVintaScheduleModelViewSet class description says it doesn’t allow updates, yet it includes UpdateModelMixin—either update the documentation or remove the conflicting mixin to align behavior and docs.

## Individual Comments

### Comment 1
<location> `calendar_integration/services/calendar_permission_service.py:63` </location>
<code_context>
+                raise ValueError("Invalid token format")
+            token_id = token_parts[0]
+            token_str = token_parts[1]
+        except (ValueError, UnicodeDecodeError) as e:
+            raise InvalidTokenError("Invalid token format") from e

</code_context>

<issue_to_address>
**suggestion (bug_risk):** Token decoding error handling could be more robust.

Also catch binascii.Error from base64.b64decode to prevent unhandled exceptions with invalid input.

Suggested implementation:

```python
import binascii

        try:
            token_full_str = base64.b64decode(token_str_base64).decode("utf-8")
            token_parts = token_full_str.split(":")
            if len(token_parts) != 2:
                raise ValueError("Invalid token format")
            token_id = token_parts[0]
            token_str = token_parts[1]

```

```python
    def initialize_with_token(self, token_str_base64: str, organization_id: int):
        try:
            token_full_str = base64.b64decode(token_str_base64).decode("utf-8")
            token_parts = token_full_str.split(":")
            if len(token_parts) != 2:
                raise ValueError("Invalid token format")

```

```python
        except (ValueError, UnicodeDecodeError, binascii.Error) as e:
            raise InvalidTokenError("Invalid token format") from e

```
</issue_to_address>

### Comment 2
<location> `calendar_integration/token_views.py:92-101` </location>
<code_context>
+        super().__init__(**kwargs)
+        self.calendar_service = calendar_service
+
+    def get_serializer_context(self):
+        """
+        Provide serializer context with token information for authentication.
+        """
+        context = super().get_serializer_context()
+
+        # Only try to extract token if there's a proper Authorization header
+        auth_header = self.request.headers.get("authorization", "")
+        if auth_header.startswith("Bearer ") and len(auth_header) > 7:
+            try:
+                organization_id = self.kwargs.get("organization_id")
+                if not organization_id:
+                    return context
+
+                organization = Organization.objects.get(id=organization_id)
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Token context extraction may leak revoked token information.

Explicitly raise PermissionDenied when a revoked token is detected to prevent silent failures and clarify authentication errors.
</issue_to_address>

### Comment 3
<location> `calendar_integration/tests/test_token_views.py:170-179` </location>
<code_context>
+    def test_create_event_with_calendar_owner_token(
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for edge cases with missing required fields in event creation.

Please add tests for scenarios where required fields are missing or invalid to verify that the API returns correct validation errors.
</issue_to_address>

### Comment 4
<location> `calendar_integration/tests/test_token_views.py:313-322` </location>
<code_context>
+    def test_update_event_with_event_token(self, client, event_token, event, organization):
</code_context>

<issue_to_address>
**suggestion (testing):** Test for updating event with invalid/missing permissions is missing.

Add a test to verify that updates are rejected with a 403 error when the token lacks UPDATE_DETAILS or RESCHEDULE permissions.

Suggested implementation:

```python
    def test_update_event_with_event_token(self, client, event_token, event, organization):
        """Test updating an event with an event-specific token."""
        token, token_str = event_token
        url = reverse(
            "calendar_token_api:token-events-detail",
            kwargs={"organization_id": organization.id, "pk": event.id},
        )

        update_data = {
            "title": "Updated Event Title",
            "description": "Updated description",
        }

        response = client.patch(
            url,
            data=json.dumps(update_data),
            content_type="application/json",
            HTTP_AUTHORIZATION=f"Bearer {token_str}",
        )

        assert response.status_code == status.HTTP_200_OK

    def test_update_event_with_insufficient_permissions(self, client, event, organization, event_token_factory):
        """
        Test that updating an event with a token lacking UPDATE_DETAILS or RESCHEDULE permissions is rejected.
        """
        # Create a token with insufficient permissions (e.g., only VIEW)
        insufficient_permissions = ["VIEW"]
        token, token_str = event_token_factory(event=event, permissions=insufficient_permissions)

        url = reverse(
            "calendar_token_api:token-events-detail",
            kwargs={"organization_id": organization.id, "pk": event.id},
        )

        update_data = {
            "title": "Should Not Update",
            "description": "Should Not Update",
        }

        response = client.patch(
            url,
            data=json.dumps(update_data),
            content_type="application/json",
            HTTP_AUTHORIZATION=f"Bearer {token_str}",
        )

        assert response.status_code == status.HTTP_403_FORBIDDEN

```

- Ensure that `event_token_factory` is available as a fixture in your test suite. If not, you will need to implement or import it. It should allow you to create event tokens with specific permissions.
- If your permission names differ (e.g., not "VIEW", "UPDATE_DETAILS", "RESCHEDULE"), adjust them to match your codebase.
</issue_to_address>

### Comment 5
<location> `calendar_integration/tests/test_token_views.py:456-465` </location>
<code_context>
+    def test_delete_event_with_event_token(self, client, event_token, event, organization):
</code_context>

<issue_to_address>
**suggestion (testing):** Consider testing deletion of non-existent events.

Add a test case for deleting a non-existent event to verify the API returns the correct error response, such as a 404.
</issue_to_address>

### Comment 6
<location> `calendar_integration/tests/test_token_views.py:609-480` </location>
<code_context>
+    def test_delete_event_with_series_parameter(self, client, event_token, event, organization):
</code_context>

<issue_to_address>
**suggestion (testing):** Test for deleting a series when the event is not part of a series is missing.

Add a test where delete_series is true for a non-series event to verify correct API behavior.

Suggested implementation:

```python
    def test_delete_event_with_series_parameter(self, client, event_token, event, organization):
        """Test deleting an event with delete_series parameter."""
        token, token_str = event_token
        url = reverse(
            "calendar_token_api:token-events-detail",
            kwargs={"organization_id": organization.id, "pk": event.id},
        )

        # Mock the calendar service delete_event method
        with patch(
            "calendar_integration.services.calendar_service.CalendarService.delete_event"
        ):
            response = client.delete(
                url + "?delete_series=true",
                HTTP_AUTHORIZATION=f"Bearer {token_str}",
            )
            assert response.status_code == status.HTTP_204_NO_CONTENT

    def test_delete_series_on_non_series_event(self, client, event_token, event, organization):
        """Test deleting a non-series event with delete_series=true parameter."""
        token, token_str = event_token
        url = reverse(
            "calendar_token_api:token-events-detail",
            kwargs={"organization_id": organization.id, "pk": event.id},
        )

        # Ensure the event is not part of a series
        assert not getattr(event, "series_id", None)

        with patch(
            "calendar_integration.services.calendar_service.CalendarService.delete_event"
        ):
            response = client.delete(
                url + "?delete_series=true",
                HTTP_AUTHORIZATION=f"Bearer {token_str}",
            )
            # Expecting either 400 Bad Request or 204 No Content depending on API design
            # Adjust assertion as per actual API behavior
            assert response.status_code in [status.HTTP_400_BAD_REQUEST, status.HTTP_204_NO_CONTENT]

```

- If your event fixture sometimes creates series events, ensure you use or create a fixture that produces a non-series event for this test.
- Adjust the expected status code in the assertion to match your API's actual behavior (e.g., 400 if it's an error, 204 if it's ignored).
</issue_to_address>

### Comment 7
<location> `calendar_integration/serializers.py:569` </location>
<code_context>


 class CalendarEventSerializer(VirtualModelSerializer):
+    user: User | None
+    token: str | None
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the serializer methods by extracting context resolution, dynamic field setup, and validation logic into dedicated helper functions.

```markdown
You’ve introduced a lot of branching into `__init__`, `validate`, `create` and `update`. You can dramatically shrink each of those methods by extracting the token/user resolution, the dynamic‐fields setup, and each validation block into small, well‐named helpers. E.g.:

```python
class CalendarEventSerializer(VirtualModelSerializer):

    def __init__(...):
        super().__init__(...)
        self._resolve_context()
        self._init_dynamic_fields()

    def _resolve_context(self):
        ctx = self.context or {}
        self.token              = ctx.get("token")
        self.token_str_base64   = ctx.get("token_str_base64")
        self.organization       = ctx.get("organization")
        self.user               = (
            None if self.token
            else getattr(ctx.get("request"), "user", None)
        )
        self.organization_id = (
            self.organization.id
            if self.organization
            else getattr(self.user, "organization_membership.organization_id", None)
        )
        self.user_is_authenticated = bool(self.token) or (
            self.user and self.user.is_authenticated
        )

    def _init_dynamic_fields(self):
        # factor out all three identical blocks into one helper
        q = lambda Model: (
            Model.objects.filter_by_organization(self.organization_id).all()
            if self.user_is_authenticated and self.organization_id
            else Model.original_manager.none()
        )
        if self.instance:
            self.fields["recurrence_rule_id"] = serializers.PrimaryKeyRelatedField(
                source="recurrence_rule_fk",
                queryset=q(RecurrenceRule),
                write_only=True,
            )
        for name, Model in [
            ("google_calendar_service_account", GoogleCalendarServiceAccount),
            ("calendar",                   Calendar),
        ]:
            self.fields[name] = serializers.PrimaryKeyRelatedField(
                queryset=q(Model),
                required=False,
                write_only=True,
            )
        # nested serializers untouched...

    def validate(self, attrs):
        self._validate_presence_of_source(attrs)
        self._validate_time_range(attrs)
        self._validate_recurrence(attrs)
        self._assign_default_calendar(attrs)
        return attrs

    def _validate_presence_of_source(self, attrs):
        if (
            not self.instance and
            not self.token and
            not any(attrs.get(k) for k in ("calendar", "provider", "google_calendar_service_account"))
        ):
            raise serializers.ValidationError(
                "You need to select either a calendar, provider, or a service account..."
            )

    def _validate_time_range(self, attrs):
        # reuse same logic for create / update
        start = attrs.get("start_time") or getattr(self.instance, "start_time_tz_unaware", None)
        end   = attrs.get("end_time")   or getattr(self.instance, "end_time_tz_unaware",   None)
        if start and end and start >= end:
            raise serializers.ValidationError("End time must be after start time.")

    def _validate_recurrence(self, attrs):
        rdata, rstring, parent = (
            attrs.get("recurrence_rule"),
            attrs.get("rrule_string"),
            attrs.get("parent_recurring_object_id"),
        )
        if rdata and rstring:
            raise serializers.ValidationError("Cannot specify both …")
        if (rdata or rstring) and parent:
            raise serializers.ValidationError("… only for master events")

    def _assign_default_calendar(self, attrs):
        if not self.instance and not attrs.get("calendar") and not self.token:
            # same logic as before, but isolated
            org_id = self.organization_id or self.user.organization_membership.organization_id
            if attrs.get("provider"):
                attrs["calendar"] = CalendarOwnership.objects.filter(
                    organization=org_id,
                    calendar__provider=attrs["provider"],
                    is_default=True,
                ).first()
            elif attrs.get("google_calendar_service_account"):
                attrs["calendar"] = attrs["google_calendar_service_account"].calendar
```

You’d apply the same pattern to `create`/`update`: pull out the “authenticate” branches into `_authenticate_token_flow` and `_authenticate_user_flow`, then each method becomes a few lines calling those helpers plus the existing pop/recurrence logic. This keeps all current functionality but makes each method easy to read and test.
</issue_to_address>

### Comment 8
<location> `calendar_integration/token_views.py:73` </location>
<code_context>
+        return calendar_service
+
+
+class TokenCalendarEventViewSet(TokenAuthenticationMixin, NoListVintaScheduleModelViewSet):
+    """
+    ViewSet for calendar event management using management tokens.
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring repeated authentication and service initialization logic into the initial() method to centralize validation and reduce duplication.

Here’s one way to collapse all of those identical try/except + `get_calendar_service(…)` calls into a single hook—override DRF’s `initial()` so it runs **before** any of your CRUD methods, and fails early if the token is invalid. You can then drop the duplicated blocks from each action:

```python
class TokenCalendarEventViewSet(TokenAuthenticationMixin, NoListVintaScheduleModelViewSet):
    # … your existing attrs …

    def initial(self, request, *args, **kwargs):
        """
        Run token-based auth & CalendarService init once, before any action.
        """
        super().initial(request, *args, **kwargs)

        org_id = kwargs.get("organization_id")
        if not org_id:
            raise PermissionDenied("Organization ID not found in URL path")

        try:
            organization = Organization.objects.get(id=org_id)
        except Organization.DoesNotExist:
            raise PermissionDenied("Invalid organization")

        # Will raise PermissionDenied if header / token is bad
        self._calendar_service = self.get_calendar_service(request, organization)

    # then drop all the try/except + get_calendar_service calls from:
    #   create, list, retrieve, update, partial_update, destroy,
    #   perform_create, perform_update, perform_destroy

    # and in those methods just call super() or serializer.save()
    def create(self, request, *args, **kwargs):
        return super().create(request, *args, **kwargs)

    def perform_destroy(self, instance):
        delete_series = self.request.query_params.get("delete_series", "false").lower() == "true"
        self._calendar_service.delete_event(
            calendar_id=instance.calendar_fk_id,
            event_id=instance.id,
            delete_series=delete_series,
        )
```

Advantages:

- All token/header/organization validation lives in one place (`initial`).
- You can store the initialized service on `self` (here `_calendar_service`) instead of re-creating it.
- CRUD methods go back to one- or two-liner overrides.
- Behavior is identical (any `PermissionDenied` raised in `initial()` returns the same 403).
</issue_to_address>

### Comment 9
<location> `calendar_integration/services/calendar_service.py:370` </location>
<code_context>
organization_id=self.organization.id
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 10
<location> `calendar_integration/tests/test_token_views.py:202-204` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid conditionals in tests. ([`no-conditionals-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-conditionals-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like conditionals, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
</issue_to_address>

### Comment 11
<location> `calendar_integration/tests/test_token_views.py:365-367` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid conditionals in tests. ([`no-conditionals-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-conditionals-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like conditionals, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
</issue_to_address>

### Comment 12
<location> `calendar_integration/tests/test_token_views.py:602-607` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid loops in tests. ([`no-loop-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-loop-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
</issue_to_address>

### Comment 13
<location> `calendar_integration/token_views.py:80` </location>
<code_context>
    authentication_classes = tuple()  # Disable default authentication - we handle it manually

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace `tuple()` with `()` ([`tuple-literal`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/tuple-literal))

```suggestion
    authentication_classes = ()  # Disable default authentication - we handle it manually
```

<br/><details><summary>Explanation</summary>The most concise and Pythonic way to create an empty tuple is to use the `()`
literal.

This fits in with the way we create tuples with items, saving a bit of
mental energy that might be taken up with thinking about two different ways of
creating tuples:

```python
x = ("first", "second")
```

Doing things this way has the added advantage of being a nice little performance
improvement. Here are the timings before and after the change:

```
$ python3 -m timeit "tuple()"
10000000 loops, best of 5: 22.6 nsec per loop
```

```
$ python3 -m timeit "()"
50000000 loops, best of 5: 5.46 nsec per loop
```
</details>
</issue_to_address>

### Comment 14
<location> `calendar_integration/token_views.py:81` </location>
<code_context>
    permission_classes = tuple()  # Disable default permissions - we handle it manually

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace `tuple()` with `()` ([`tuple-literal`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/tuple-literal))

```suggestion
    permission_classes = ()  # Disable default permissions - we handle it manually
```

<br/><details><summary>Explanation</summary>The most concise and Pythonic way to create an empty tuple is to use the `()`
literal.

This fits in with the way we create tuples with items, saving a bit of
mental energy that might be taken up with thinking about two different ways of
creating tuples:

```python
x = ("first", "second")
```

Doing things this way has the added advantage of being a nice little performance
improvement. Here are the timings before and after the change:

```
$ python3 -m timeit "tuple()"
10000000 loops, best of 5: 22.6 nsec per loop
```

```
$ python3 -m timeit "()"
50000000 loops, best of 5: 5.46 nsec per loop
```
</details>
</issue_to_address>

### Comment 15
<location> `calendar_integration/serializers.py:795-799` </location>
<code_context>
    def validate(self, attrs):
        calendar = attrs.get("calendar")

        # For token-based requests, we can infer the calendar from the token context
        # Only check for calendar/provider/service account during creation for non-token requests
        if (
            not self.instance  # This is a creation, not an update
            and not calendar
            and not attrs.get("provider")
            and not attrs.get("google_calendar_service_account")
            and not self.token  # No token present - require explicit calendar/provider/service account
        ):
            raise serializers.ValidationError(
                "You need to select either a calendar, provider, or a service account to create "
                "an event."
            )

        # Validate start_time and end_time only if both are provided or if this is a creation
        start_time = attrs.get("start_time")
        end_time = attrs.get("end_time")

        # For updates, use existing instance values if not provided in attrs
        if self.instance:
            start_time = start_time or self.instance.start_time_tz_unaware
            end_time = end_time or self.instance.end_time_tz_unaware

        if start_time and end_time and start_time >= end_time:
            raise serializers.ValidationError("End time must be after start time.")

        # Validate recurrence fields
        recurrence_rule_data = attrs.get("recurrence_rule")
        rrule_string = attrs.get("rrule_string")
        parent_recurring_object_id = attrs.get("parent_recurring_object_id")

        if recurrence_rule_data and rrule_string:
            raise serializers.ValidationError(
                "Cannot specify both recurrence_rule and rrule_string. Use one or the other."
            )

        if (recurrence_rule_data or rrule_string) and parent_recurring_object_id:
            raise serializers.ValidationError(
                "Cannot specify recurrence rule for event instances. Recurrence rules are only for master events."
            )

        # Only auto-assign calendar during creation, not updates
        if not calendar and not self.instance:
            if self.token:
                # For token-based requests, we'll infer the calendar in the create method
                # after the calendar service is initialized, so we can skip calendar assignment here
                pass
            else:
                # Use stored user or organization from regular authentication
                if self.user and hasattr(self.user, "organization_membership"):
                    organization_id = self.user.organization_membership.organization_id
                elif self.organization:
                    organization_id = self.organization.id
                else:
                    raise serializers.ValidationError(
                        "Cannot determine organization for calendar selection."
                    )

                if attrs.get("provider"):
                    attrs["calendar"] = CalendarOwnership.objects.filter(
                        organization=organization_id,
                        calendar__provider=attrs.get("provider"),
                        is_default=True,
                    ).first()
                elif attrs.get("google_calendar_service_account"):
                    attrs["calendar"] = attrs.get("google_calendar_service_account").calendar

        return attrs

</code_context>

<issue_to_address>
**suggestion (code-quality):** Swap if/else to remove empty if body ([`remove-pass-body`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-pass-body/))

```suggestion
            if not self.token:
```
</issue_to_address>

### Comment 16
<location> `calendar_integration/serializers.py:824` </location>
<code_context>
    def create(self, validated_data):
        if not self.calendar_service:
            raise CalendarServiceNotInjectedError(
                "calendar_service is not defined, please configure your DI container correctly"
            )

        calendar: Calendar | None = validated_data.pop("calendar", None)

        # Use token or user for authentication
        if self.token_str_base64:
            # Token-based authentication - initialize without provider
            # We need to get the organization from the token context first
            organization = self.organization
            if not organization:
                raise serializers.ValidationError(
                    "Organization context is required for token-based authentication"
                )

            self.calendar_service.initialize_without_provider(
                user_or_token=self.token_str_base64, organization=organization
            )

            # If no calendar was provided, infer it from the token after service initialization
            if not calendar:
                # The calendar service now has the permission service initialized with the token
                permission_service = self.calendar_service.calendar_permission_service
                if permission_service and permission_service.token:
                    if permission_service.token.calendar_fk:
                        # Calendar-level token - use the token's calendar
                        calendar = permission_service.token.calendar_fk
                    elif permission_service.token.event_fk:
                        # Event-level token - use the event's calendar
                        calendar = permission_service.token.event_fk.calendar_fk
                    else:
                        raise serializers.ValidationError(
                            "Unable to determine calendar from token context."
                        )
                else:
                    raise serializers.ValidationError(
                        "Token authentication failed or calendar could not be determined."
                    )
        elif self.user:
            # Regular user authentication
            if not calendar:
                raise serializers.ValidationError(
                    "Calendar is required for user-based authentication"
                )

            user = self.user
            if validated_data.get("google_calendar_service_account"):
                account = validated_data.get("google_calendar_service_account")
                self.calendar_service.authenticate(
                    account=account,
                    organization=calendar.organization,
                )
            else:
                self.calendar_service.authenticate(
                    account=user,
                    organization=calendar.organization,
                )
        else:
            raise serializers.ValidationError(
                {
                    "non_field_errors": [
                        "You need either an authenticated user or a Calendar Management Token"
                    ]
                }
            )

        resource_allocations = validated_data.pop("resource_allocations", [])
        attendances = validated_data.pop("attendances", [])
        external_attendances = validated_data.pop("external_attendances", [])

        # Handle recurrence fields
        recurrence_rule_data = validated_data.pop("recurrence_rule", None)
        rrule_string = validated_data.pop("rrule_string", None)
        parent_recurring_object_id = validated_data.pop("parent_recurring_object_id", None)

        # Prepare recurrence rule for calendar service
        final_rrule_string = None
        if recurrence_rule_data:
            # Convert recurrence_rule_data to RRULE string
            temp_rule = RecurrenceRule(organization=calendar.organization, **recurrence_rule_data)
            final_rrule_string = temp_rule.to_rrule_string()
        elif rrule_string:
            final_rrule_string = rrule_string

        event = self.calendar_service.create_event(
            calendar_id=calendar.id,
            event_data=CalendarEventInputData(
                title=validated_data.get("title"),
                description=validated_data.get("description"),
                start_time=validated_data.get("start_time"),
                end_time=validated_data.get("end_time"),
                timezone=validated_data.get("timezone"),
                resource_allocations=[
                    ResourceAllocationInputData(resource_id=ra["calendar"].id)
                    for ra in resource_allocations
                ],
                attendances=[
                    EventAttendanceInputData(user_id=att["user"].id) for att in attendances
                ],
                external_attendances=[
                    EventExternalAttendanceInputData(
                        external_attendee=ExternalAttendeeInputData(
                            id=ext["external_attendee"].get("id"),
                            email=ext["external_attendee"]["email"],
                            name=ext["external_attendee"]["name"],
                        )
                    )
                    for ext in external_attendances
                ],
                # Recurrence fields
                recurrence_rule=final_rrule_string,
                parent_event_id=parent_recurring_object_id,
                is_recurring_exception=validated_data.get("is_recurring_exception", False),
            ),
        )

        return event

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/inline-immediately-returned-variable/))
- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
- Remove unnecessary else after guard condition ([`remove-unnecessary-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-else/))
</issue_to_address>

### Comment 17
<location> `calendar_integration/services/calendar_permission_service.py:63` </location>
<code_context>
    def initialize_with_token(self, token_str_base64: str, organization_id: int):
        try:
            token_full_str = base64.b64decode(token_str_base64).decode("utf-8")
            token_parts = token_full_str.split(":")
            if len(token_parts) != 2:
                raise ValueError("Invalid token format")
            token_id = token_parts[0]
            token_str = token_parts[1]
        except (ValueError, UnicodeDecodeError) as e:
            raise InvalidTokenError("Invalid token format") from e

        try:
            token = (
                CalendarManagementToken.objects.prefetch_related("permissions")
                .select_related("calendar", "event")
                .filter(organization_id=organization_id)
                .get(id=token_id, revoked_at__isnull=True)
            )
        except CalendarManagementToken.DoesNotExist as e:
            # Handle any exceptions that occur during initialization
            raise InvalidTokenError("Invalid token string provided") from e

        if not verify_long_lived_token(token_str, token.token_hash):
            raise InvalidTokenError("Invalid token string provided") from None

        self.token = token

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Remove redundant exceptions from an except clause ([`remove-redundant-exception`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-exception/))
- Replace length-one exception tuple with exception ([`simplify-single-exception-tuple`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-single-exception-tuple/))

```suggestion
        except ValueError as e:
```
</issue_to_address>

### Comment 18
<location> `calendar_integration/tests/test_token_views.py:333` </location>
<code_context>
    def test_update_event_with_event_token(self, client, event_token, event, organization):
        """Test updating an event with an event-specific token."""
        token, token_str = event_token
        url = reverse(
            "calendar_token_api:token-events-detail",
            kwargs={"organization_id": organization.id, "pk": event.id},
        )

        update_data = {
            "title": "Updated Event Title",
            "description": "Updated description",
        }

        response = client.patch(
            url,
            data=json.dumps(update_data),
            content_type="application/json",
            HTTP_AUTHORIZATION=self.create_auth_header(token.id, token_str),
        )

        assert response.status_code == status.HTTP_200_OK

        # Verify event was updated
        event.refresh_from_db()
        assert event.title == "Updated Event Title"
        assert event.description == "Updated description"
        """Test updating an event with an event-specific token."""
        token, token_str = event_token
        url = reverse(
            "calendar_token_api:token-events-detail",
            kwargs={"organization_id": organization.id, "pk": event.id},
        )

        update_data = {
            "title": "Updated Event Title",
            "description": "Updated description",
        }

        # Mock the calendar service update_event method
        with patch(
            "calendar_integration.services.calendar_service.CalendarService.update_event"
        ) as mock_update_event:
            mock_update_event.return_value = event

            response = client.patch(
                url,
                data=json.dumps(update_data),
                content_type="application/json",
                HTTP_AUTHORIZATION=self.create_auth_header(token.id, token_str),
            )

        # Debug the response if it's not 200
        if response.status_code != status.HTTP_200_OK:
            print(f"Response status: {response.status_code}")
            print(f"Response content: {response.content.decode()}")

        assert response.status_code == status.HTTP_200_OK

        # Verify event was updated
        event.refresh_from_db()
        assert event.title == "Updated Event Title"
        assert event.description == "Updated description"

</code_context>

<issue_to_address>
**issue (code-quality):** Extract duplicate code into method ([`extract-duplicate-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-duplicate-method/))
</issue_to_address>

### Comment 19
<location> `calendar_integration/token_views.py:47-49` </location>
<code_context>
    def extract_token_from_header(self, request: Request) -> tuple[str, int]:
        """
        Extract and validate basic token format from Authorization header.
        Returns (token_str_base64, organization_id) from the URL path.
        """
        auth_header = request.headers.get("authorization", "")
        if not auth_header.startswith("Bearer "):
            raise PermissionDenied(
                "Missing or invalid Authorization header. Expected: Bearer <token>"
            )

        token_str_base64 = auth_header[7:]  # Remove 'Bearer ' prefix
        if not token_str_base64:
            raise PermissionDenied("Missing token in Authorization header")

        # Extract organization_id from URL path
        if not request.resolver_match or not request.resolver_match.kwargs:
            raise PermissionDenied("Organization ID not found in URL path")

        organization_id = request.resolver_match.kwargs.get("organization_id")
        if not organization_id:
            raise PermissionDenied("Organization ID not found in URL path")

        return token_str_base64, int(organization_id)

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
</issue_to_address>

### Comment 20
<location> `calendar_integration/token_views.py:129-135` </location>
<code_context>
    def get_serializer_context(self):
        """
        Provide serializer context with token information for authentication.
        """
        context = super().get_serializer_context()

        # Only try to extract token if there's a proper Authorization header
        auth_header = self.request.headers.get("authorization", "")
        if auth_header.startswith("Bearer ") and len(auth_header) > 7:
            try:
                organization_id = self.kwargs.get("organization_id")
                if not organization_id:
                    return context

                organization = Organization.objects.get(id=organization_id)

                # Get the token and extract the token object
                token_str_base64, _ = self.extract_token_from_header(self.request)
                decoded_token = base64.b64decode(token_str_base64).decode()
                token_id, _ = decoded_token.split(":", 1)

                # Get the token object with organization filter
                token = CalendarManagementToken.objects.filter_by_organization(organization.id).get(
                    id=token_id
                )

                # Check if token is revoked
                if token.revoked_at is not None:
                    return context  # Don't include token info for revoked tokens

                # Pass token string and organization to serializer context
                context["token"] = token
                context[
                    "token_str_base64"
                ] = token_str_base64  # For calendar service authentication
                context["organization"] = organization

            except (
                ValueError,
                CalendarManagementToken.DoesNotExist,
                Organization.DoesNotExist,
                UnicodeDecodeError,
                PermissionDenied,
            ):
                # If token is invalid, silently continue without token context
                # The individual HTTP methods will handle authentication and return proper error responses
                pass

        return context

</code_context>

<issue_to_address>
**suggestion (code-quality):** Remove redundant exceptions from an except clause ([`remove-redundant-exception`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-exception/))

```suggestion
            except (ValueError, CalendarManagementToken.DoesNotExist, Organization.DoesNotExist, PermissionDenied):
```
</issue_to_address>

### Comment 21
<location> `calendar_integration/token_views.py:146-147` </location>
<code_context>
    def get_queryset(self):
        """
        Filter events based on organization from the URL.
        """
        organization_id = self.kwargs.get("organization_id")
        if organization_id:
            return CalendarEvent.objects.filter_by_organization(organization_id)
        return CalendarEvent.objects.none()

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if organization_id := self.kwargs.get("organization_id"):
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +92 to +101
def get_serializer_context(self):
"""
Provide serializer context with token information for authentication.
"""
context = super().get_serializer_context()

# Only try to extract token if there's a proper Authorization header
auth_header = self.request.headers.get("authorization", "")
if auth_header.startswith("Bearer ") and len(auth_header) > 7:
try:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 suggestion (security): Token context extraction may leak revoked token information.

Explicitly raise PermissionDenied when a revoked token is detected to prevent silent failures and clarify authentication errors.

return calendar_service


class TokenCalendarEventViewSet(TokenAuthenticationMixin, NoListVintaScheduleModelViewSet):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring repeated authentication and service initialization logic into the initial() method to centralize validation and reduce duplication.

Here’s one way to collapse all of those identical try/except + get_calendar_service(…) calls into a single hook—override DRF’s initial() so it runs before any of your CRUD methods, and fails early if the token is invalid. You can then drop the duplicated blocks from each action:

class TokenCalendarEventViewSet(TokenAuthenticationMixin, NoListVintaScheduleModelViewSet):
    # … your existing attrs …

    def initial(self, request, *args, **kwargs):
        """
        Run token-based auth & CalendarService init once, before any action.
        """
        super().initial(request, *args, **kwargs)

        org_id = kwargs.get("organization_id")
        if not org_id:
            raise PermissionDenied("Organization ID not found in URL path")

        try:
            organization = Organization.objects.get(id=org_id)
        except Organization.DoesNotExist:
            raise PermissionDenied("Invalid organization")

        # Will raise PermissionDenied if header / token is bad
        self._calendar_service = self.get_calendar_service(request, organization)

    # then drop all the try/except + get_calendar_service calls from:
    #   create, list, retrieve, update, partial_update, destroy,
    #   perform_create, perform_update, perform_destroy

    # and in those methods just call super() or serializer.save()
    def create(self, request, *args, **kwargs):
        return super().create(request, *args, **kwargs)

    def perform_destroy(self, instance):
        delete_series = self.request.query_params.get("delete_series", "false").lower() == "true"
        self._calendar_service.delete_event(
            calendar_id=instance.calendar_fk_id,
            event_id=instance.id,
            delete_series=delete_series,
        )

Advantages:

  • All token/header/organization validation lives in one place (initial).
  • You can store the initialized service on self (here _calendar_service) instead of re-creating it.
  • CRUD methods go back to one- or two-liner overrides.
  • Behavior is identical (any PermissionDenied raised in initial() returns the same 403).

and isinstance(self.user_or_token, str)
):
self.calendar_permission_service.initialize_with_token(
self.user_or_token, organization_id=self.organization.id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security (generic-api-key): Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Source: gitleaks


serializer_class = CalendarEventSerializer
queryset = CalendarEvent.objects.all()
authentication_classes = tuple() # Disable default authentication - we handle it manually
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Replace tuple() with () (tuple-literal)

Suggested change
authentication_classes = tuple() # Disable default authentication - we handle it manually
authentication_classes = () # Disable default authentication - we handle it manually


ExplanationThe most concise and Pythonic way to create an empty tuple is to use the ()
literal.

This fits in with the way we create tuples with items, saving a bit of
mental energy that might be taken up with thinking about two different ways of
creating tuples:

x = ("first", "second")

Doing things this way has the added advantage of being a nice little performance
improvement. Here are the timings before and after the change:

$ python3 -m timeit "tuple()"
10000000 loops, best of 5: 22.6 nsec per loop
$ python3 -m timeit "()"
50000000 loops, best of 5: 5.46 nsec per loop

serializer_class = CalendarEventSerializer
queryset = CalendarEvent.objects.all()
authentication_classes = tuple() # Disable default authentication - we handle it manually
permission_classes = tuple() # Disable default permissions - we handle it manually
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Replace tuple() with () (tuple-literal)

Suggested change
permission_classes = tuple() # Disable default permissions - we handle it manually
permission_classes = () # Disable default permissions - we handle it manually


ExplanationThe most concise and Pythonic way to create an empty tuple is to use the ()
literal.

This fits in with the way we create tuples with items, saving a bit of
mental energy that might be taken up with thinking about two different ways of
creating tuples:

x = ("first", "second")

Doing things this way has the added advantage of being a nice little performance
improvement. Here are the timings before and after the change:

$ python3 -m timeit "tuple()"
10000000 loops, best of 5: 22.6 nsec per loop
$ python3 -m timeit "()"
50000000 loops, best of 5: 5.46 nsec per loop

Comment on lines +795 to +799
if self.token:
# For token-based requests, we'll infer the calendar in the create method
# after the calendar service is initialized, so we can skip calendar assignment here
pass
else:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Swap if/else to remove empty if body (remove-pass-body)

Suggested change
if self.token:
# For token-based requests, we'll infer the calendar in the create method
# after the calendar service is initialized, so we can skip calendar assignment here
pass
else:
if not self.token:

raise ValueError("Invalid token format")
token_id = token_parts[0]
token_str = token_parts[1]
except (ValueError, UnicodeDecodeError) as e:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): We've found these issues:

Suggested change
except (ValueError, UnicodeDecodeError) as e:
except ValueError as e:

Comment on lines +129 to +135
except (
ValueError,
CalendarManagementToken.DoesNotExist,
Organization.DoesNotExist,
UnicodeDecodeError,
PermissionDenied,
):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Remove redundant exceptions from an except clause (remove-redundant-exception)

Suggested change
except (
ValueError,
CalendarManagementToken.DoesNotExist,
Organization.DoesNotExist,
UnicodeDecodeError,
PermissionDenied,
):
except (ValueError, CalendarManagementToken.DoesNotExist, Organization.DoesNotExist, PermissionDenied):

Comment on lines +146 to +147
organization_id = self.kwargs.get("organization_id")
if organization_id:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
organization_id = self.kwargs.get("organization_id")
if organization_id:
if organization_id := self.kwargs.get("organization_id"):

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Oct 10, 2025

Codecov Report

❌ Patch coverage is 69.03766% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.15%. Comparing base (414a7b4) to head (411e902).
⚠️ Report is 23 commits behind head on main.

Files with missing lines Patch % Lines
calendar_integration/serializers.py 42.64% 29 Missing and 10 partials ⚠️
calendar_integration/token_views.py 77.08% 22 Missing and 11 partials ⚠️
...ntegration/services/calendar_permission_service.py 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #13      +/-   ##
==========================================
- Coverage   81.19%   81.15%   -0.05%     
==========================================
  Files         143      145       +2     
  Lines        7769     7970     +201     
  Branches      857      884      +27     
==========================================
+ Hits         6308     6468     +160     
- Misses       1100     1131      +31     
- Partials      361      371      +10     
Flag Coverage Δ
unittests 81.15% <69.03%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@hugobessa hugobessa merged commit 8ffb932 into main Oct 10, 2025
1 of 2 checks passed
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.

2 participants