-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
xRPC app.arkavo.actor.getProfile (#21)
* Add Profile Lambda and migrate project to new structure Introduced a new Profile Lambda function for retrieving user profiles in compliance with the ATProtocol. Added DynamoDB support via ProfilesTable and updated permissions accordingly. Restructured the project by renaming and reorganizing the directory to better align with Lambda-centric workflows. * Refactor profile lambda and update deployment docs Simplified response formatting and error handling in the profile lambda, improving readability and maintainability. Modified routes to remove redundant path prefixes. Updated README with deployment planning steps and renamed it for consistency.
- Loading branch information
1 parent
946ce9b
commit e2b8fe2
Showing
13 changed files
with
455 additions
and
1 deletion.
There are no files selected for viewing
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
# Arkavo Profile Lambda | ||
|
||
ATProtocol-compliant Lambda function for retrieving user profile information. This service provides a scalable endpoint for accessing both standard ATProtocol profile fields and Arkavo-specific extensions. | ||
|
||
## Overview | ||
|
||
The Profile Lambda function serves as an endpoint for retrieving user profile information through the ATProtocol specification. It interfaces with DynamoDB to store and retrieve profile data, handling both standard ATProtocol fields and Arkavo-specific extensions. | ||
|
||
### Endpoint Details | ||
|
||
- **URL**: `https://xrpc.arkavo.net/xrpc/app.arkavo.actor.getProfile` | ||
- **Method**: GET | ||
- **Query Parameters**: | ||
- `actor` (required): String representing either the DID or handle of the user | ||
|
||
## Getting Started | ||
|
||
### Prerequisites | ||
|
||
- Python 3.9+ | ||
- AWS CLI configured with appropriate credentials | ||
- AWS SAM CLI (for local development) | ||
|
||
### Installation | ||
|
||
1. Clone the repository: | ||
```bash | ||
git clone https://github.com/arkavo/devsecops.git | ||
cd lambdas/profile | ||
``` | ||
|
||
2. Create a virtual environment and install dependencies: | ||
```bash | ||
python3.12 -m venv .venv | ||
source .venv/bin/activate | ||
pip install -r requirements.txt | ||
``` | ||
|
||
3. Set up local environment variables: | ||
```bash | ||
cp .env.example .env | ||
# Edit .env with your configuration | ||
``` | ||
|
||
### Local Development | ||
|
||
Run the function locally using SAM CLI: | ||
|
||
```bash | ||
sam local invoke -e events/test_event.json | ||
``` | ||
|
||
Or start a local API endpoint: | ||
|
||
```bash | ||
sam local start-api | ||
``` | ||
|
||
## Deployment | ||
|
||
### Using AWS SAM | ||
|
||
1. Build the application: | ||
```bash | ||
sam build | ||
``` | ||
|
||
2. Deploy to AWS: | ||
```bash | ||
sam deploy --guided | ||
``` | ||
|
||
### Manual Deployment | ||
|
||
1. Package the Lambda: | ||
```bash | ||
zip -r function.zip . -x "*.git*" "*.env*" "*.venv*" | ||
``` | ||
|
||
2. Deploy using AWS CLI: | ||
```bash | ||
aws lambda update-function-code --function-name profile-lambda --zip-file fileb://function.zip | ||
``` | ||
|
||
## Configuration | ||
|
||
### Environment Variables | ||
|
||
- `PROFILES_TABLE_NAME`: DynamoDB table name for storing profiles | ||
|
||
### DynamoDB Schema | ||
|
||
The DynamoDB table requires the following schema: | ||
|
||
```json | ||
{ | ||
"actor": "string" (Primary Key), | ||
"handle": "string", | ||
"did": "string", | ||
"displayName": "string", | ||
"avatarUrl": "string" (optional), | ||
"description": "string" (optional), | ||
"profileType": "string", | ||
"isVerified": "boolean", | ||
"followersCount": "number", | ||
"followingCount": "number", | ||
"creationDate": "string", | ||
"publicID": "string" | ||
} | ||
``` | ||
|
||
## API Response Format | ||
|
||
### Successful Response | ||
|
||
```json | ||
{ | ||
"standard": { | ||
"handle": "string", | ||
"did": "string", | ||
"displayName": "string", | ||
"avatarUrl": "string", | ||
"description": "string" | ||
}, | ||
"extended": { | ||
"profileType": "string", | ||
"isVerified": boolean, | ||
"followersCount": number, | ||
"followingCount": number, | ||
"creationDate": "string", | ||
"publicID": "string" | ||
} | ||
} | ||
``` | ||
|
||
### Error Response | ||
|
||
```json | ||
{ | ||
"error": "Error message" | ||
} | ||
``` | ||
|
||
## Testing | ||
|
||
Run the test suite: | ||
|
||
```bash | ||
pytest tests/ | ||
``` | ||
|
||
Run with coverage: | ||
|
||
```bash | ||
pytest --cov=src tests/ | ||
``` | ||
|
||
## Monitoring and Logging | ||
|
||
The function uses AWS Lambda Powertools for structured logging and tracing. Logs are available in CloudWatch Logs, and traces can be viewed in AWS X-Ray. | ||
|
||
### Log Groups | ||
|
||
- `/aws/lambda/profile-lambda`: Main function logs | ||
- `/aws/lambda/profile-lambda-dev`: Development environment logs | ||
|
||
### Metrics | ||
|
||
Key metrics are available in CloudWatch Metrics under the custom namespace `Arkavo/ProfileLambda`: | ||
|
||
- `GetProfileLatency`: Response time for profile retrieval | ||
- `ProfileNotFound`: Count of 404 errors | ||
- `DatabaseErrors`: Count of DynamoDB-related errors | ||
|
||
## Related Documentation | ||
|
||
- [ATProtocol Specification](https://atproto.com/specs/atp) | ||
- [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/) | ||
- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import os | ||
from typing import Dict, Any, Optional | ||
|
||
import boto3 | ||
from aws_lambda_powertools import Logger, Metrics | ||
from aws_lambda_powertools.event_handler import APIGatewayRestResolver | ||
from aws_lambda_powertools.utilities.typing import LambdaContext | ||
from botocore.exceptions import ClientError | ||
|
||
# Initialize utilities | ||
logger = Logger() | ||
metrics = Metrics() | ||
app = APIGatewayRestResolver(strip_prefixes=["/xrpc"]) | ||
|
||
# Constants | ||
DYNAMO_TABLE_NAME = os.environ["PROFILES_TABLE_NAME"] | ||
|
||
|
||
class ProfileError(Exception): | ||
"""Custom exception for profile-related errors""" | ||
|
||
def __init__(self, message: str, status_code: int = 400): | ||
self.message = message | ||
self.status_code = status_code | ||
super().__init__(self.message) | ||
|
||
|
||
def get_profile_from_dynamo(actor: str) -> Optional[Dict[str, Any]]: | ||
""" | ||
Retrieve profile from DynamoDB | ||
""" | ||
dynamodb = boto3.resource("dynamodb") | ||
table = dynamodb.Table(DYNAMO_TABLE_NAME) | ||
|
||
try: | ||
# First try as handle (primary key) | ||
response = table.get_item(Key={"handle": actor}) | ||
if "Item" in response: | ||
return response["Item"] | ||
|
||
# If not found, scan for DID or publicID | ||
response = table.scan( | ||
FilterExpression="did = :did OR publicID = :pid", | ||
ExpressionAttributeValues={ | ||
":did": actor, | ||
":pid": actor | ||
} | ||
) | ||
items = response.get("Items", []) | ||
return items[0] if items else None | ||
except ClientError as e: | ||
logger.error(f"DynamoDB error: {str(e)}") | ||
raise ProfileError("Database error", 500) | ||
|
||
|
||
def validate_profile_data(profile: Dict[str, Any]) -> None: | ||
""" | ||
Validate profile data structure and required fields | ||
""" | ||
required_fields = { | ||
"handle", "did", "profileName", "creationDate", "publicID" | ||
} | ||
|
||
missing_fields = required_fields - set(profile.keys()) | ||
if missing_fields: | ||
raise ProfileError(f"Missing required fields: {', '.join(missing_fields)}") | ||
|
||
|
||
def format_profile_response(profile: Dict[str, Any]) -> Dict[str, Any]: | ||
""" | ||
Format profile data as a flat structure | ||
""" | ||
return { | ||
"handle": profile["handle"], | ||
"did": profile["did"], | ||
"displayName": profile["profileName"], | ||
"avatarUrl": profile.get("avatarUrl"), | ||
"description": profile.get("description"), | ||
"creationDate": profile["creationDate"], | ||
"publicID": profile["publicID"] | ||
} | ||
|
||
|
||
@app.get("/app.arkavo.actor.getProfile") | ||
def get_profile() -> Dict[str, Any]: | ||
""" | ||
Main handler for profile retrieval | ||
""" | ||
try: | ||
# Get and validate query parameters | ||
actor = app.current_event.get_query_string_value(name="actor") | ||
if not actor: | ||
app.response.status_code = 400 | ||
return {"error": "Missing 'actor' parameter"} | ||
|
||
# Get profile data | ||
profile = get_profile_from_dynamo(actor) | ||
if not profile: | ||
app.response.status_code = 404 | ||
return {"error": "Profile not found"} | ||
|
||
# Validate profile data | ||
validate_profile_data(profile) | ||
|
||
# Record metric for successful lookup | ||
metrics.add_metric(name="ProfileLookupSuccess", unit="Count", value=1) | ||
|
||
# Return formatted response | ||
return format_profile_response(profile) | ||
|
||
except ProfileError as error: | ||
logger.error(f"Profile error: {error.message}") | ||
metrics.add_metric(name="ProfileLookupError", unit="Count", value=1) | ||
app.response.status_code = error.status_code | ||
return {"error": error.message} | ||
except Exception as e: | ||
logger.exception("Unhandled exception") | ||
metrics.add_metric(name="UnhandledError", unit="Count", value=1) | ||
app.response.status_code = 500 | ||
return {"error": "Internal server error"} | ||
|
||
|
||
@logger.inject_lambda_context | ||
@metrics.log_metrics | ||
def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]: | ||
""" | ||
Lambda handler entry point | ||
""" | ||
return app.resolve(event, context) | ||
|
||
|
||
if __name__ == "__main__": | ||
# For local testing | ||
test_event = { | ||
"queryStringParameters": {"actor": "test_user"} | ||
} | ||
print(lambda_handler(test_event, None)) |
Oops, something went wrong.