Skip to content

Conversation

@BrandtKruger
Copy link
Contributor

Explain your changes

fix: correct singularization for user_identities and identities

Fix singularization bug that generated incorrect method names for
user_identities and identities endpoints. Add _singularize_resource()
helper to handle special cases and maintain backward compatibility.

Fixes: create_user_identitie → create_user_identity
get_identitie → get_identity
update_identitie → update_identity
delete_identitie → delete_identity

Checklist

🛟 If you need help, consider asking for advice over in the Kinde community.

lexthink and others added 2 commits October 19, 2025 03:02
- Update response types for various endpoints
- Add new endpoints for user identities management
Fix the singularization bug that was generating incorrect method names:
- create_user_identitie → create_user_identity
- get_identitie → get_identity
- update_identitie → update_identity
- delete_identitie → delete_identity

Changes:
- Add _singularize_resource() helper method to centralize singularization logic
- Handle special cases: user_identities → user_identity, identities → identity
- Update both _generate_methods() and _create_api_method() to use the helper
- Maintains existing behavior for other resources (users, organizations, etc.)

This fixes a pre-existing bug that was exposed by PR kinde-oss#128's addition of
user_identities and identities endpoints.
@BrandtKruger BrandtKruger requested a review from a team as a code owner November 26, 2025 17:36
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

Updated ManagementClient endpoints to use pluralized resource paths, added identity/user_identity endpoints, introduced _singularize_resource for singularizing plural names used in dynamic method generation, changed some HTTP verbs and RESPONSE_TYPES, and replaced GET-based feature-flag retrieval with a POST-based create_feature_flag. Tests updated accordingly.

Changes

Cohort / File(s) Summary
Management client API surface & method generation
kinde_sdk/management/management_client.py
Reworked API_ENDPOINTS to use pluralized resource paths (e.g., /organizations, /roles, /feature_flags, /applications), adjusted several HTTP verbs (some PATCH → PUT), modified RESPONSE_TYPES/status-code mappings and response type names, and added _singularize_resource(self, resource: str) -> str. Dynamic method generation now uses the singularizer for resource-name derivation and path parameter handling.
Feature flags & identity endpoints
kinde_sdk/management/management_client.py
Changed feature-flag flow from GET/list/get to POST-based creation (create_feature_flag) and introduced new endpoints for user_identities (list/create) and identities (get/update/delete).
Tests: management client behavior and feature-flag flow
testv2/testv2_management/test_management_client.py
Removed tests using removed methods (get_permission, get_feature_flag, update_api_application), added/updated tests to use create_feature_flag (POST to feature_flags with body {key,type,value}) and adjusted expected response shapes and docstrings to match endpoint/name changes.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as ManagementClient
    participant Gen as _generate_methods()
    participant Sing as _singularize_resource()
    participant APIMethod as generated API method
    participant Server as Kinde API

    Dev->>Gen: initialize -> generate dynamic methods
    Gen->>Sing: singularize(resource_name)
    Sing-->>Gen: singular form (handles special cases)
    Gen->>APIMethod: emit API caller (path, verb, response mapping)
    Dev->>APIMethod: call create_feature_flag(key,type,value)
    APIMethod->>Server: POST /feature_flags with body {key,type,value}
    Server-->>APIMethod: 200 + payload
    APIMethod-->>Dev: return mapped response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect _singularize_resource() special-case rules for correctness and coverage.
  • Verify API_ENDPOINTS plural paths and updated HTTP verbs match API contract.
  • Confirm RESPONSE_TYPES and status-code changes align with server responses and tests.
  • Review tests in testv2/testv2_management/test_management_client.py for correct expectations of create_feature_flag payload/response.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Pr 128' is vague and does not convey meaningful information about the actual changes, failing to summarize the singularization bug fix or any other substantive change. Use a descriptive title that summarizes the main change, such as 'Fix singularization for user_identities and identities endpoints' or 'Add _singularize_resource() helper for correct method naming'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly relates to the changeset, explaining the singularization bug fix and detailing specific method name corrections, which aligns with the changes in the API endpoints and the new _singularize_resource() helper.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Update test assertions to reflect removed endpoints:
- Remove get_permission assertion (permissions no longer has get endpoint)
- Remove get_feature_flags and get_feature_flag assertions (removed in PR kinde-oss#128)
- Remove update_api_application assertion (removed in PR kinde-oss#128)
- Update test_feature_flags_api_calls to test create_feature_flag instead

These changes align tests with the updated Management API endpoints
introduced in PR kinde-oss#128.
Copy link
Contributor

@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)
kinde_sdk/management/management_client.py (2)

387-408: Incomplete handling of -ies plural forms.

The method correctly handles user_identities and identities, but other resources ending in -ies would produce incorrect singular forms:

  • industriesindustrie (should be industry)
  • propertiespropertie (should be property)

Currently these resources only have list actions so no singular methods are generated, but this could break if singular actions are added later.

Consider a more robust approach:

     def _singularize_resource(self, resource: str) -> str:
         # Handle special cases
         if resource == 'business':
             return 'business'  # Don't remove 's' from 'business'
         elif resource == 'user_identities':
             return 'user_identity'  # Replace 'identities' with 'identity'
         elif resource == 'identities':
             return 'identity'  # Replace 'identities' with 'identity'
+        # Handle words ending in 'ies' -> 'y' (e.g., properties -> property)
+        elif resource.endswith('ies'):
+            return resource[:-3] + 'y'
         # Default: remove trailing 's' if present
         elif resource.endswith('s'):
             return resource[:-1]
         else:
             return resource

474-480: Dead code: manual query string construction is unused.

The final_path variable constructed here is never used. The actual query parameter handling is done by param_serialize at line 489, which receives query_params directly.

Consider removing this unused code block:

-            # FIXED: Use param_serialize to properly construct the full URL with host
-            # Handle query parameters by appending them to the path
-            final_path = formatted_path
-            if query_params and http_method in ('GET', 'DELETE'):
-                query_string = '&'.join([f"{k}={v}" for k, v in query_params.items() if v is not None])
-                if query_string:
-                    separator = '&' if '?' in final_path else '?'
-                    final_path = f"{final_path}{separator}{query_string}"
-            
             # Use param_serialize to get the proper URL with host
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7deb113 and 56d744f.

📒 Files selected for processing (1)
  • kinde_sdk/management/management_client.py (11 hunks)
🧰 Additional context used
🪛 GitHub Actions: CI
kinde_sdk/management/management_client.py

[error] 1-1: Dynamic method generation missing 'get_permission' method. The ManagementClient should expose a 'get_permission' method for permissions API.


[error] 1-1: ManagementClient does not implement get_feature_flags; only has create_feature_flag as per tests.

🔇 Additional comments (3)
kinde_sdk/management/management_client.py (3)

207-215: LGTM! New identity endpoints correctly added.

The new user_identities and identities resources are properly structured with appropriate paths and HTTP methods. This addresses the PR objective of fixing the singularization bug for these endpoints.


348-356: LGTM!

Response type mappings for the new identity endpoints are correctly structured with appropriate status codes (201 for create, 200 for others).


410-434: LGTM!

The method generation logic correctly uses the centralized _singularize_resource helper, ensuring consistent singular form derivation for all dynamically generated method names.

@codecov
Copy link

codecov bot commented Nov 26, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
testv2/testv2_management/test_management_client.py (1)

491-533: Feature flag test now matches create‑via‑POST behavior

The test_feature_flags_api_calls update correctly models the new create_feature_flag flow: POST to /feature_flags with a {"key", "type", "value"} body and a single-flag response object, and the test wiring (token, deserialize, assertion) is consistent with other tests in this file.

If you want this test to guard more strongly against regressions, consider also asserting on mock_api_client_instance.param_serialize.call_args (HTTP method, URL, and body) rather than just assert_called_once(), but that’s optional given the existing patterns.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56d744f and 65f0413.

📒 Files selected for processing (1)
  • testv2/testv2_management/test_management_client.py (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-04T07:09:21.430Z
Learnt from: brettchaldecott
Repo: kinde-oss/kinde-python-sdk PR: 83
File: kinde_sdk/auth/feature_flags.py:85-102
Timestamp: 2025-07-04T07:09:21.430Z
Learning: In the Kinde Python SDK v2, the maintainer (brettchaldecott) prefers to keep async keywords on methods in the feature flags module even when they don't currently perform await operations, as this approach has been tested and is preferred for the SDK's design.

Applied to files:

  • testv2/testv2_management/test_management_client.py
📚 Learning: 2025-08-21T04:46:50.368Z
Learnt from: KomanRudden
Repo: kinde-oss/kinde-python-sdk PR: 101
File: kinde_sdk/auth/permissions.py:6-7
Timestamp: 2025-08-21T04:46:50.368Z
Learning: The kinde_sdk has both frontend and management API modules. The frontend permissions_api.py exists at kinde_sdk/frontend/api/permissions_api.py and contains PermissionsApi class. Using the management API instead of the frontend API for user permissions would be incorrect.

Applied to files:

  • testv2/testv2_management/test_management_client.py
🔇 Additional comments (1)
testv2/testv2_management/test_management_client.py (1)

98-120: Comment updates correctly capture removed management methods

The new notes about get_permission, get_feature_flags / get_feature_flag, and update_api_application being removed align with the updated ManagementClient surface and help future readers understand why those methods aren’t asserted here. No functional changes introduced in this block; looks good as-is.

Add the missing 'list' endpoint to feature_flags resource to enable
get_feature_flags() method generation.

Changes:
- Add 'list': ('GET', '/api/v1/feature_flags') to feature_flags API_ENDPOINTS
- Add corresponding response type mapping: 'list': {'200': 'GetFeatureFlagsResponse', ...}
- Update test to assert get_feature_flags method exists

This addresses the CodeRabbit review comment on PR kinde-oss#143 and enables
the SDK to generate the get_feature_flags() method as documented in
the Kinde Management API.
…nse types

- Corrected endpoint paths for organizations and API applications to use plural forms.
- Updated response types for create endpoints to reflect correct HTTP status codes (201).
- Adjusted test assertions to ensure alignment with the updated API structure.

These changes enhance consistency with the Kinde Management API specifications.
Copy link
Contributor

@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: 1

♻️ Duplicate comments (1)
kinde_sdk/management/management_client.py (1)

77-83: permissions still lacks a get action / get_permission mapping

The permissions resource only defines list/create/update/delete and has no 'get' entry in either API_ENDPOINTS or RESPONSE_TYPES. A prior review noted this was causing a missing get_permission method and pipeline failures; that appears unchanged here. If your API and consumers expect a get_permission method, you’ll still need to add:

  • "'get': ('GET', '/api/v1/permissions/{permission_id}')," under API_ENDPOINTS['permissions']
  • A corresponding 'get' mapping under RESPONSE_TYPES['permissions'] using the appropriate single‑permission response model.

Also applies to: 258-263

🧹 Nitpick comments (1)
kinde_sdk/management/management_client.py (1)

174-178: Minor: update_business docstring suggests an ID param that the endpoint does not use

For the business resource, update is defined as PATCH /api/v1/business with no path parameter. The generic docstring generator for action == 'update' will still describe an ID argument (business_id), but the generated method accepts only *args, **kwargs and ignores any positional ID because the path has no {} placeholder. If you want the docs to match behavior, consider special‑casing this resource/action in the docstring generation or adjusting the wording to omit the ID parameter.

Also applies to: 327-330

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65f0413 and dafb06b.

📒 Files selected for processing (2)
  • kinde_sdk/management/management_client.py (12 hunks)
  • testv2/testv2_management/test_management_client.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • testv2/testv2_management/test_management_client.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-21T04:46:50.368Z
Learnt from: KomanRudden
Repo: kinde-oss/kinde-python-sdk PR: 101
File: kinde_sdk/auth/permissions.py:6-7
Timestamp: 2025-08-21T04:46:50.368Z
Learning: The kinde_sdk has both frontend and management API modules. The frontend permissions_api.py exists at kinde_sdk/frontend/api/permissions_api.py and contains PermissionsApi class. Using the management API instead of the frontend API for user permissions would be incorrect.

Applied to files:

  • kinde_sdk/management/management_client.py
📚 Learning: 2025-07-04T07:09:21.430Z
Learnt from: brettchaldecott
Repo: kinde-oss/kinde-python-sdk PR: 83
File: kinde_sdk/auth/feature_flags.py:85-102
Timestamp: 2025-07-04T07:09:21.430Z
Learning: In the Kinde Python SDK v2, the maintainer (brettchaldecott) prefers to keep async keywords on methods in the feature flags module even when they don't currently perform await operations, as this approach has been tested and is preferred for the SDK's design.

Applied to files:

  • kinde_sdk/management/management_client.py
🔇 Additional comments (2)
kinde_sdk/management/management_client.py (2)

41-41: Pluralized create endpoints and new identity routes look coherent—please confirm against the Management API spec

The shifts to plural resource paths (organizations, roles, permissions, feature_flags, applications, apis, subscribers, properties) and the new user_identities / identities routes are consistent with the rest of the mapping and should generate the expected method names (e.g., create_user_identity, get_identity, update_identity, delete_identity). Please double‑check these paths and verbs (especially the PATCH→PUT changes) against the live Management API docs so the generated client stays in sync with the server.

Also applies to: 72-72, 80-80, 88-89, 97-97, 106-106, 114-114, 131-131, 177-178, 208-216


389-411: New _singularize_resource helper cleanly fixes the identitie method-name bug

Using _singularize_resource with explicit cases for business, user_identities, and identities ensures that dynamic methods are now correctly named (create_user_identity, get_identity, update_identity, delete_identity) while leaving existing resources’ method names stable. Reusing the same helper in _generate_methods and _create_api_method keeps docstrings and method names aligned. This directly addresses the prior singularization issue without introducing obvious regressions.

Also applies to: 415-415, 451-451, 208-216, 354-357

'list': {'200': 'UsersResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
'get': {'200': 'User', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
'create': {'201': 'CreateUserResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
'create': {'200': 'CreateUserResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if management_client.py exists and get file size
find . -name "management_client.py" -type f

Repository: kinde-oss/kinde-python-sdk

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Get line count and read the relevant sections
wc -l ./kinde_sdk/management/management_client.py

Repository: kinde-oss/kinde-python-sdk

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Read the relevant sections of the file to see the status code mappings
sed -n '220,360p' ./kinde_sdk/management/management_client.py | cat -n

Repository: kinde-oss/kinde-python-sdk

Length of output: 13362


🏁 Script executed:

#!/bin/bash
# Search for test files related to management client
find . -name "*test*.py" -o -name "*spec*.py" | grep -i management | head -20

Repository: kinde-oss/kinde-python-sdk

Length of output: 426


🏁 Script executed:

#!/bin/bash
# Check the size of test files and look for relevant tests
wc -l ./test_management_client.py ./testv2/testv2_management/test_management_client.py

Repository: kinde-oss/kinde-python-sdk

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Search for tests related to status codes and the operations mentioned in the review
echo "=== Searching for create/update/webhooks/subscribers/api_applications tests ==="
rg -n "users.*create|roles.*update|webhooks|subscribers.*create|api_applications|connected_apps" ./test_management_client.py ./testv2/testv2_management/test_management_client.py -A 3 -B 1 | head -100

Repository: kinde-oss/kinde-python-sdk

Length of output: 2696


🏁 Script executed:

#!/bin/bash
# Search for OpenAPI/Swagger specs or API documentation
find . -name "*.yaml" -o -name "*.yml" -o -name "*.json" | grep -i "spec\|openapi\|swagger\|api" | head -20

Repository: kinde-oss/kinde-python-sdk

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Look for models or response definitions
find . -name "*model*.py" -o -name "*response*.py" | head -20

Repository: kinde-oss/kinde-python-sdk

Length of output: 1319


🏁 Script executed:

#!/bin/bash
# Check the api_response.py file and search for response_deserialize logic
wc -l ./kinde_sdk/management/api_response.py
cat -n ./kinde_sdk/management/api_response.py

Repository: kinde-oss/kinde-python-sdk

Length of output: 910


🏁 Script executed:

#!/bin/bash
# Search for response_deserialize in the management client
rg -n "response_deserialize" ./kinde_sdk/management/management_client.py -A 20 -B 5

Repository: kinde-oss/kinde-python-sdk

Length of output: 1517


🏁 Script executed:

#!/bin/bash
# Find where api_client is initialized and its type
rg -n "self.api_client" ./kinde_sdk/management/management_client.py | head -10

Repository: kinde-oss/kinde-python-sdk

Length of output: 432


🏁 Script executed:

#!/bin/bash
# Search for ApiClient class and response_deserialize method
find . -name "*.py" -type f | xargs grep -l "class ApiClient" | head -5

Repository: kinde-oss/kinde-python-sdk

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Read api_client.py and search for response_deserialize
wc -l ./kinde_sdk/management/api_client.py
rg -n "def response_deserialize" ./kinde_sdk/management/api_client.py -A 30

Repository: kinde-oss/kinde-python-sdk

Length of output: 1631


🏁 Script executed:

#!/bin/bash
# Read more of response_deserialize to understand full error handling
sed -n '286,350p' ./kinde_sdk/management/api_client.py | cat -n

Repository: kinde-oss/kinde-python-sdk

Length of output: 3366


🌐 Web query:

Kinde Management API documentation roles update status code response

💡 Result:

Short answer

  • Successful update: HTTP 200 OK (JSON body with success info). [1][2]
  • Validation/client errors: HTTP 4xx with JSON error object (example: {"code":"PERMISSIONS_REQUIRED","message":"Permissions parameter is required"}). [3]
  • Other common responses: 401/403 for auth/scopes, 429 for rate limiting, 5xx for server errors. See rate-limit handling headers. [1][4]

Sources

  1. Kinde Management API reference / APIs overview. [docs.kinde.com]. [1]
  2. Kinde "Manage user roles" (describes updating roles via Management API). [2]
  3. Community example showing a PATCH error response with PERMISSIONS_REQUIRED. [3]
  4. Kinde API rate-limits and 429 handling. [4]

🌐 Web query:

Kinde Management API users create POST response status code 200 201

💡 Result:

Short answer: Kinde’s Management API docs don’t explicitly state a single required success status for the create-user endpoint; community examples and the official SDK show the POST /v1/user (or /api/v1/user) returns the created user object on success, but implementations in the wild may return either 200 OK or 201 Created. Check the actual response status on your tenant (inspect the HTTP response) and rely on the presence of the created-user JSON (and Location header if present) rather than assuming only 200 or only 201. Sources: Kinde API docs and community threads. [1][2]

Sources

  • Kinde Management API docs (API reference / management). [1]
  • Community reports / Q&A about POST /v1/user behavior. [2]

Fix incorrect status code mapping for roles.update and verify other operations match actual API responses

The Kinde Management API documentation confirms that successful update operations return HTTP 200 OK, not 201. However, roles.update is currently mapped to '201': 'SuccessResponse', which will cause response_deserialize to fail matching the response status code. When the API returns 200 but the mapping expects 201, the deserialization skips silently (response_type remains None), resulting in callers receiving data=None instead of the actual response.

Additionally, the mappings for several other operations require verification against actual Kinde API responses:

  • users.create (currently 200) — confirm whether the API actually returns 200 or 201
  • webhooks.create (currently 200) — verify if create endpoints consistently return 200
  • subscribers.create (currently 201) — confirm this is correct
  • api_applications.create (currently 201) — confirm this is correct
  • connected_apps.update (currently 200) — verify update operations
  • Identity models (GetIdentitiesResponse, CreateIdentityResponse, Identity) — confirm these exist and match API responses

Mismatches between mapped status codes and actual API responses will cause silent deserialization failures.

🤖 Prompt for AI Agents
In kinde_sdk/management/management_client.py around line 224, the response
status mapping for roles.update is incorrectly set to '201' which causes
response_deserialize to miss a 200 OK response; change the mapping to use '200'
for roles.update and then audit the surrounding operation mappings to ensure
they match the API (verify users.create, webhooks.create, subscribers.create,
api_applications.create, connected_apps.update) and update their expected status
codes to the actual API responses (200 vs 201) as needed; also confirm the
referenced identity models (GetIdentitiesResponse, CreateIdentityResponse,
Identity) exist and match API shapes, update model names or imports if
necessary, and run integration/unit tests or a quick live call to each endpoint
to validate deserialization returns data instead of None.

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