Skip to content

Commit 5b6424f

Browse files
committed
feat: Bring Python project to feature parity with Node.js version
This commit brings the Python AppLink starter project to full feature parity with the Node.js version, including a more robust application structure, comprehensive API functionality, and complete test coverage. ### Major Changes - **Restructured Application:** - Migrated from a single `main.py` file to a scalable, modular structure using FastAPI's `APIRouter`. - Code is now organized into `app/routers` for clear separation of concerns. - Updated `Procfile` to reflect the new application entry point (`app.main:app`). - **Implemented Full API Feature Set:** - Added `/unitofwork` endpoint to demonstrate creating multiple related records in Salesforce using the Unit of Work pattern. - Added `/handleDataCloudDataChangeEvent` endpoint to serve as a webhook for Data Cloud Data Actions. - Added a standard `/health` check endpoint. - Configured FastAPI to serve interactive API documentation via Swagger UI at `/docs`, using the existing `api-spec.yaml`. - **Added Robust Testing:** - Created a comprehensive test suite using `pytest`. - Implemented a robust testing strategy using `conftest.py` to mock the `heroku_applink` SDK, resolving complex dependency and plugin conflicts. - Added tests for all API endpoints, including happy paths and error conditions. - Achieved 100% test coverage for all application logic. - **Improved Developer Experience:** - Created `bin/invoke.py`, a Python-based equivalent of the Node.js `invoke.sh` script, for easy local testing with Salesforce context headers. - Completely rewrote the `README.md` to be as comprehensive as the Node.js version, with updated instructions for setup, local development, testing, and manual deployment. - Added a `pytest.ini` file to standardize test execution. - Refined `requirements.txt` to ensure stable dependency resolution. - **Enhanced Code Quality and Documentation:** - Audited and refactored the entire codebase to adhere to Python best practices. - Added comprehensive Sphinx-style docstrings to all modules and functions. - Implemented full type hinting across the application for improved clarity and static analysis. - Introduced Pydantic response models for better data validation and more accurate API documentation. - Fixed Pydantic deprecation warnings by updating `Field` definitions. ### Motivation The goal was to elevate the Python starter project from a minimal example to a feature-complete and robust template that fully demonstrates the power of Heroku AppLink. This provides a much stronger foundation for developers looking to build Salesforce-integrated applications in Python, mirroring the quality and completeness of the Node.js template.
1 parent fc4263b commit 5b6424f

File tree

18 files changed

+748
-65
lines changed

18 files changed

+748
-65
lines changed

.DS_Store

6 KB
Binary file not shown.

.coverage

52 KB
Binary file not shown.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ __pycache__/
22
.pytest_cache/
33
.venv/
44
venv/
5+
htmlcov/
6+
commit-message.txt

Procfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web: export APP_PORT=3000 && heroku-applink-service-mesh-latest-amd64 --port $PORT -- uvicorn main:app --port=$APP_PORT
1+
web: uvicorn app.main:app --host 0.0.0.0 --port $PORT

README.md

Lines changed: 153 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,186 @@
1-
# FastAPI Heroku AppLink Integration
1+
# Heroku AppLink Python App Template
22

3-
This is a basic FastAPI application that demonstrates integration with Heroku AppLink.
3+
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://www.heroku.com/deploy?template=https://github.com/heroku-reference-apps/applink-getting-started-python)
44

5-
## Local Setup
5+
The Heroku AppLink Python app template is a [FastAPI](https://fastapi.tiangolo.com/) web application that demonstrates how to build APIs for Salesforce integration using Heroku AppLink. This template includes authentication, authorization, and API specifications for seamless integration with Salesforce, Data Cloud, and Agentforce.
6+
7+
## Table of Contents
8+
9+
- [Quick Start](#quick-start)
10+
- [Local Development](#local-development)
11+
- [Testing with invoke.py](#testing-with-invokepy)
12+
- [Running Automated Tests](#running-automated-tests)
13+
- [Manual Heroku Deployment](#manual-heroku-deployment)
14+
- [Heroku AppLink Setup](#heroku-applink-setup)
15+
- [Project Structure](#project-structure)
16+
- [API Documentation](#api-documentation)
17+
- [Additional Resources](#additional-resources)
18+
19+
## Quick Start
20+
21+
### Prerequisites
22+
23+
- Python 3.8+
24+
- `pip` for package management
25+
- Git
26+
- Heroku CLI (for deployment)
27+
- Salesforce org (for AppLink integration)
28+
29+
### Deploy to Heroku (One-Click)
30+
31+
Click the Deploy button above to deploy this app directly to Heroku with the AppLink add-on pre-configured.
32+
33+
## Local Development
34+
35+
### 1. Clone and Install
636

7-
1. Install the required dependencies:
837
```bash
38+
git clone https://github.com/heroku-reference-apps/applink-getting-started-python.git
39+
cd applink-getting-started-python
40+
python -m venv venv
41+
source venv/bin/activate
942
pip install -r requirements.txt
1043
```
1144

12-
2. Run the application:
45+
### 2. Start the Development Server
46+
1347
```bash
14-
uvicorn main:app --reload
48+
uvicorn app.main:app --reload
1549
```
1650

17-
The application will be available at `http://localhost:8000`
51+
Your app will be available at `http://localhost:8000`.
52+
53+
### 3. API Endpoints
54+
55+
- **GET /accounts** - Retrieve Salesforce accounts from the invoking org.
56+
- **POST /unitofwork** - Create a unit of work for Salesforce.
57+
- **POST /handleDataCloudDataChangeEvent** - Handle a Salesforce Data Cloud Change Event.
58+
- **GET /docs** - Interactive Swagger UI for API documentation.
59+
- **GET /health** - Health check endpoint.
60+
61+
### 4. View API Documentation
62+
63+
Visit `http://localhost:8000/docs` to explore the interactive API documentation powered by Swagger UI.
1864

19-
## Heroku Deployment
65+
## Testing with invoke.py
66+
67+
The `bin/invoke.py` script allows you to test your locally running app with proper Salesforce client context headers.
68+
69+
### Usage
2070

21-
1. Make sure you have the Heroku CLI installed and are logged in:
2271
```bash
23-
heroku login
72+
./bin/invoke.py ORG_DOMAIN ACCESS_TOKEN ORG_ID USER_ID [METHOD] [API_PATH] [--data DATA]
2473
```
2574

26-
2. Create a new Heroku app:
75+
### Parameters
76+
77+
- **ORG_DOMAIN**: Your Salesforce org domain (e.g., `mycompany.my.salesforce.com`)
78+
- **ACCESS_TOKEN**: Valid Salesforce access token
79+
- **ORG_ID**: Salesforce organization ID (15 or 18 characters)
80+
- **USER_ID**: Salesforce user ID (15 or 18 characters)
81+
- **METHOD**: HTTP method (default: GET)
82+
- **API_PATH**: API endpoint path (default: /accounts)
83+
- **--data**: JSON data for POST/PUT requests (as a string)
84+
85+
### Examples
86+
2787
```bash
88+
# Test the accounts endpoint
89+
./bin/invoke.py mycompany.my.salesforce.com TOKEN_123 00D123456789ABC 005123456789ABC
90+
91+
# Test with POST data
92+
./bin/invoke.py mycompany.my.salesforce.com TOKEN_123 00D123456789ABC 005123456789ABC POST /unitofwork --data '{"data":{"accountName":"Test Account", "lastName":"Test", "subject":"Test Case"}}'
93+
94+
# Test custom endpoint
95+
./bin/invoke.py mycompany.my.salesforce.com TOKEN_123 00D123456789ABC 005123456789ABC GET /health
96+
```
97+
98+
### Getting Salesforce Credentials
99+
100+
To get the required Salesforce credentials for testing:
101+
102+
1. **Access Token**: Use Salesforce CLI to generate a session token (`sf org display --target-org <alias> --json | jq .result.accessToken -r`).
103+
2. **Org ID**: Found in Setup → Company Information or by running `sf org display --target-org <alias> --json | jq .result.id -r`.
104+
3. **User ID**: Found in your user profile or Setup → Users or by running `sf org display --target-org <alias> --json | jq .result.userId -r`.
105+
106+
## Running Automated Tests
107+
108+
This project uses `pytest` for unit testing. To run the tests:
109+
110+
```bash
111+
pytest
112+
```
113+
114+
## Manual Heroku Deployment
115+
116+
### 1. Prerequisites
117+
118+
- [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed
119+
- Git repository initialized
120+
- Heroku account with billing enabled (for add-ons)
121+
122+
### 2. Create Heroku App
123+
124+
```bash
125+
# Create a new Heroku app
28126
heroku create your-app-name
127+
128+
# Or let Heroku generate a name
129+
heroku create
29130
```
30131

31-
3. Deploy to Heroku:
132+
### 3. Add Required Buildpacks
133+
134+
The app requires two buildpacks in the correct order:
135+
32136
```bash
33-
git add .
34-
git commit -m "Initial commit"
35-
git push heroku main
137+
# Add the AppLink Service Mesh buildpack first
138+
heroku buildpacks:add heroku/heroku-applink-service-mesh
139+
140+
# Add the Python buildpack second
141+
heroku buildpacks:add heroku/python
36142
```
37143

38-
4. Configure Heroku AppLink:
144+
### 4. Provision the AppLink Add-on
145+
39146
```bash
147+
# Provision the Heroku AppLink add-on
40148
heroku addons:create heroku-applink
149+
150+
# Set the required HEROKU_APP_ID config var
151+
heroku config:set HEROKU_APP_ID="$(heroku apps:info --json | jq -r '.app.id')"
41152
```
42153

43-
5. Open your application:
154+
### 5. Deploy the Application
155+
44156
```bash
157+
# Deploy to Heroku
158+
git push heroku main
159+
160+
# Check deployment status
161+
heroku ps:scale web=1
45162
heroku open
46163
```
47164

48-
## Endpoints
165+
### 6. Verify Deployment
166+
167+
```bash
168+
# Check app logs
169+
heroku logs --tail
170+
```
171+
172+
## Heroku AppLink Setup
173+
174+
(Instructions for `heroku salesforce:connect`, `heroku salesforce:authorizations:add`, and `heroku salesforce:publish` are identical to the Node.js version and can be referenced from the official documentation.)
175+
176+
## Additional Resources
177+
178+
### Documentation
49179

50-
- `GET /`: Returns a simple root page
51-
- `GET /accounts`: Queries accounts using Heroku AppLink Data API
180+
- [Getting Started with Heroku AppLink](https://devcenter.heroku.com/articles/getting-started-heroku-applink)
181+
- [Heroku AppLink CLI Plugin](https://devcenter.heroku.com/articles/heroku-applink-cli)
182+
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
52183

53-
## Note
184+
---
54185

55-
This application requires proper Heroku AppLink configuration to work with the Data API. Make sure you have the necessary credentials and configuration set up in your environment.
186+
**Note**: This template is designed for educational purposes and as a starting point for building Salesforce-integrated applications. For production use, ensure proper error handling, security measures, and testing practices are implemented.

app/main.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
This module is the main entry point for the AppLink Python Starter aplication.
3+
4+
It initializes the FastAPI application, loads the OpenAPI specification,
5+
and includes the API routers.
6+
"""
7+
import heroku_applink as sdk
8+
from fastapi import FastAPI, Response
9+
from typing import Dict, Any
10+
11+
from .routers import accounts, unitofwork, datacloud
12+
13+
import yaml
14+
15+
config = sdk.Config()
16+
17+
app = FastAPI(
18+
title="AppLink Python Starter",
19+
description="A starter project for building Salesforce-integrated applications with Heroku AppLink.",
20+
version="1.0.0"
21+
)
22+
23+
with open("api-spec.yaml", "r") as f:
24+
openapi_schema = yaml.safe_load(f)
25+
app.openapi_schema = openapi_schema
26+
27+
app.add_middleware(sdk.IntegrationAsgiMiddleware, config=config)
28+
29+
app.include_router(accounts.router)
30+
app.include_router(unitofwork.router)
31+
app.include_router(datacloud.router)
32+
33+
@app.get("/health", summary="Health check endpoint")
34+
def get_health() -> Dict[str, str]:
35+
"""
36+
A simple health check endpoint.
37+
38+
:return: A dictionary with a status of "ok".
39+
:rtype: Dict[str, str]
40+
"""
41+
return {"status": "ok"}

app/routers/accounts.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
This router handles the /accounts endpoint.
3+
"""
4+
import heroku_applink as sdk
5+
from fastapi import APIRouter
6+
from pydantic import BaseModel
7+
from typing import List, Dict, Any
8+
9+
router = APIRouter(
10+
prefix="/accounts",
11+
tags=["accounts"],
12+
)
13+
14+
class Account(BaseModel):
15+
id: str
16+
name: str
17+
18+
async def _query_accounts(data_api: Any) -> Any:
19+
"""
20+
Private helper function to query for accounts.
21+
22+
:param data_api: The Data API client from the Heroku AppLink context.
23+
:type data_api: Any
24+
:return: The result of the SOQL query.
25+
:rtype: Any
26+
"""
27+
query = "SELECT Id, Name FROM Account"
28+
result = await data_api.query(query)
29+
# The print statement is for debugging and can be removed in production.
30+
for record in result.records:
31+
print("===== account record", record)
32+
return result
33+
34+
@router.get("/", response_model=List[Account])
35+
async def get_accounts() -> List[Account]:
36+
"""
37+
Queries for and then returns all Accounts in the invoking org.
38+
39+
This endpoint demonstrates how to use the Heroku AppLink SDK to perform
40+
a simple SOQL query against the invoking Salesforce organization.
41+
42+
:return: A list of Account objects.
43+
:rtype: List[Account]
44+
"""
45+
data_api = sdk.get_client_context().data_api
46+
result = await _query_accounts(data_api)
47+
48+
accounts = [
49+
Account(id=record.fields["Id"], name=record.fields["Name"])
50+
for record in result.records
51+
]
52+
return accounts

app/routers/datacloud.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
This router handles the /handleDataCloudDataChangeEvent endpoint, which acts as a
3+
webhook for Salesforce Data Cloud Data Action events.
4+
"""
5+
from fastapi import APIRouter, Request, Response
6+
from pydantic import BaseModel, Field
7+
from typing import List, Dict, Any, Optional
8+
import heroku_applink as sdk
9+
import os
10+
11+
router = APIRouter(
12+
prefix="/handleDataCloudDataChangeEvent",
13+
tags=["datacloud"],
14+
)
15+
16+
class DataCloudEvent(BaseModel):
17+
ActionDeveloperName: str
18+
EventType: str
19+
EventPrompt: str
20+
SourceObjectDeveloperName: str
21+
EventPublishDateTime: str
22+
PayloadCurrentValue: Dict[str, Any]
23+
24+
class DataCloudSchema(BaseModel):
25+
schemaId: str
26+
27+
class DataCloudRequest(BaseModel):
28+
events: List[DataCloudEvent]
29+
schemas: List[DataCloudSchema]
30+
31+
@router.post("/", status_code=204)
32+
async def handle_data_cloud_change_event(request: DataCloudRequest) -> None:
33+
"""
34+
Handle Data Cloud Data Action events.
35+
36+
This endpoint is designed to be a webhook target for Data Cloud Data Actions.
37+
It parses the incoming event payload and, if environment variables are set,
38+
can query a connected Data Cloud org.
39+
40+
:param request: The incoming Data Cloud event payload.
41+
:type request: DataCloudRequest
42+
:return: An empty response with a 204 status code.
43+
:rtype: None
44+
"""
45+
context = sdk.get_client_context()
46+
logger = context.logger
47+
data_cloud = context.data_cloud
48+
49+
action_event = data_cloud.parse_data_action_event(request.model_dump())
50+
logger.info(
51+
f"POST /dataCloudDataChangeEvent: {action_event.count} events for schemas "
52+
f"{[s.schemaId for s in action_event.schemas] if action_event.schemas else 'n/a'}"
53+
)
54+
55+
for evt in action_event.events:
56+
logger.info(
57+
f"Got action '{evt.ActionDeveloperName}', event type '{evt.EventType}' "
58+
f"triggered by {evt.EventPrompt} on object '{evt.SourceObjectDeveloperName}' "
59+
f"published on {evt.EventPublishDateTime}"
60+
)
61+
# In a real application, you would add logic here to handle
62+
# the changed object values from `evt.PayloadCurrentValue`.
63+
64+
if os.environ.get("DATA_CLOUD_ORG") and os.environ.get("DATA_CLOUD_QUERY"):
65+
org_name = os.environ["DATA_CLOUD_ORG"]
66+
query = os.environ["DATA_CLOUD_QUERY"]
67+
app_link_addon = context.addons.applink
68+
69+
logger.info(f"Getting '{org_name}' org connection from Heroku AppLink add-on...")
70+
org = await app_link_addon.get_authorization(org_name)
71+
72+
logger.info(f"Querying org '{org_name}' ({org.id}): {query}")
73+
response = await org.data_cloud_api.query(query)
74+
logger.info(f"Query response: {response.data}")
75+
76+
return

0 commit comments

Comments
 (0)