452 lines
11 KiB
Plaintext
452 lines
11 KiB
Plaintext
---
|
|
title: Python 3
|
|
icon: SiPython
|
|
---
|
|
|
|
Welcome to the Duckity Python SDK documentation! This guide will teach you how to install and set up
|
|
the SDK in no time.
|
|
|
|
The SDK is async-first. To call it from synchronous code, wrap the calls in `asyncio.run()` or your
|
|
event loop's version of it.
|
|
|
|
<Callout>
|
|
All SDKs are fully compatible with each other. Solution tokens generated in one SDK will be
|
|
validated just fine in other SDKs, including this one.
|
|
</Callout>
|
|
|
|
## Quick Start
|
|
|
|
Before you can integrate Duckity into your application, you'll need to have the following:
|
|
|
|
1. An application,
|
|
2. At least one protection profile created in that application, and
|
|
3. The ID of the protection profiles to use
|
|
|
|
If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
|
|
read the [Quick Start](/quick-start) guide to learn how to set those up.
|
|
|
|
Once you got those ready, follow these steps to get things running on your client:
|
|
|
|
### Install the SDK [step]
|
|
|
|
Install the SDK from PyPI using your favorite package manager:
|
|
|
|
{/* prettier-ignore */}
|
|
<Tabs items={["pip", "uv", "poetry"]} groupId="python-package-manager" persist>
|
|
<Tab value="pip" id="pip">
|
|
```sh
|
|
pip install duckity
|
|
```
|
|
</Tab>
|
|
<Tab value="uv" id="uv">
|
|
```sh
|
|
uv add duckity
|
|
```
|
|
</Tab>
|
|
<Tab value="poetry" id="poetry">
|
|
```sh
|
|
poetry add duckity
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### Solve a Challenge [step]
|
|
|
|
Solving a challenge only requires a protection profile ID.
|
|
|
|
<Tabs items={["Async", "Sync"]} groupId="python-sync" persist>
|
|
<Tab value="Async" id="async">
|
|
```py
|
|
import duckity
|
|
|
|
solution: str = await duckity.solve(PROTECTION_PROFILE_ID)
|
|
```
|
|
|
|
The CPU-intensive part of solving the challenge is done in a `ProcessPoolExecutor()`.
|
|
Neither the GIL nor the async event loop will be blocked.
|
|
</Tab>
|
|
<Tab value="Sync" id="sync">
|
|
```py
|
|
import asyncio
|
|
|
|
import duckity
|
|
|
|
solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID))
|
|
```
|
|
|
|
The CPU-intensive part of solving the challenge is done in a `ProcessPoolExecutor()`. The
|
|
GIL will not be blocked.
|
|
</Tab>
|
|
|
|
</Tabs>
|
|
|
|
### Validate a Solution [step]
|
|
|
|
Validation is done server-side. You can send the solution token to your server any way you want; A
|
|
JSON field or an HTTP header is usually convenient.
|
|
|
|
To validate a solution token, you'll need 4 things:
|
|
|
|
1. The solution token,
|
|
2. The IP of the client that submitted the solution,
|
|
3. The application's secret, and
|
|
4. The protection profile's ID.
|
|
|
|
Once you have them, you can validate a solution token as follows:
|
|
|
|
<Tabs items={["Async", "Sync"]} groupId="python-sync" persist>
|
|
<Tab value="Async" id="async">
|
|
```py
|
|
import duckity
|
|
|
|
solution: str
|
|
client_ip: str
|
|
application_secret: str
|
|
protection_profile_id: str
|
|
|
|
is_valid: bool = await duckity.validate(
|
|
solution, client_ip, application_secret, protection_profile_id
|
|
)
|
|
```
|
|
</Tab>
|
|
<Tab value="Sync" id="sync">
|
|
```py
|
|
import asyncio
|
|
|
|
import duckity
|
|
|
|
solution: str
|
|
client_ip: str
|
|
application_secret: str
|
|
protection_profile_id: str
|
|
|
|
is_valid: bool = asyncio.run(
|
|
duckity.validate(solution, client_ip, application_secret, protection_profile_id)
|
|
)
|
|
```
|
|
</Tab>
|
|
|
|
</Tabs>
|
|
|
|
That's it! If the solution token is valid, you can proceed to process your request. If it's not,
|
|
return an error to the client and do not further process the request.
|
|
|
|
## Advanced Usage
|
|
|
|
Solving a challenge on demand works well for simple setups. However, UX can be greatly improved
|
|
changing a few settings and planning when to solve challenges.
|
|
|
|
### Asynchronous Challenge Solving
|
|
|
|
The challenge does not need to wait for the user to finish filling up a form or completing an action
|
|
to be issued. When you can guess the user will need a solution token, it is a good idea to start
|
|
computing it before the user needs it.
|
|
|
|
For example, if the user is logging in to a backend service via a CLI, you can fetch and solve a
|
|
challenge while the user is filling up their username and password. For example:
|
|
|
|
```py
|
|
import asyncio
|
|
|
|
import getpass
|
|
|
|
import duckity
|
|
|
|
|
|
async def login():
|
|
solution_task = asyncio.create_task(duckity.solve(PROTECTION_PROFILE_ID))
|
|
|
|
email = await asyncio.to_thread(input, "Enter your email: ")
|
|
password = await asyncio.to_thread(getpass.getpass, "Enter your password (hidden): ")
|
|
|
|
print("Logging you in...")
|
|
|
|
solution = await solution_task
|
|
|
|
# Log the user in here...
|
|
|
|
|
|
asyncio.run(login())
|
|
```
|
|
|
|
## Integrations
|
|
|
|
Duckity's Python SDK works anywhere out of the box. These examples show how to integrate it to some
|
|
common frameworks and tools.
|
|
|
|
### FastAPI
|
|
|
|
```py lineNumbers
|
|
import os
|
|
|
|
import dotenv
|
|
|
|
import duckity
|
|
|
|
from fastapi import FastAPI, Request, HTTPException
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
dotenv.load_dotenv(override=True)
|
|
|
|
|
|
PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
|
|
APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
class RequestBodySchema(BaseModel):
|
|
solution: str
|
|
|
|
|
|
@app.post("/protected")
|
|
async def protected(request: Request, payload: RequestBodySchema):
|
|
is_valid = await duckity.validate(
|
|
payload.solution,
|
|
request.client.host, # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
|
|
APPLICATION_SECRET,
|
|
PROTECTION_PROFILE_ID
|
|
)
|
|
|
|
if not is_valid:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="The provided solution token was invalid.",
|
|
)
|
|
|
|
return {
|
|
"message": "This is protected!"
|
|
}
|
|
```
|
|
|
|
### Flask
|
|
|
|
```py lineNumbers
|
|
import os
|
|
|
|
import dotenv
|
|
|
|
import duckity
|
|
|
|
from flask import Flask, request, jsonify
|
|
|
|
|
|
dotenv.load_dotenv(override=True)
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
|
|
APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
|
|
|
|
|
|
@app.post("/protected")
|
|
async def protected():
|
|
solution = request.json.get("solution")
|
|
|
|
if not solution:
|
|
return jsonify({"error": "Missing solution"}), 400
|
|
|
|
client_ip = request.remote_addr # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
|
|
|
|
is_valid = await duckity.validate(
|
|
solution,
|
|
client_ip,
|
|
APPLICATION_SECRET,
|
|
PROTECTION_PROFILE_ID,
|
|
)
|
|
|
|
if not is_valid:
|
|
return jsonify({"error": "Invalid solution"}), 403
|
|
|
|
return jsonify({"message": "This is protected!"})
|
|
```
|
|
|
|
### Django
|
|
|
|
```py lineNumbers
|
|
import json
|
|
|
|
import duckity
|
|
|
|
from django.conf import settings
|
|
from django.http import JsonResponse
|
|
|
|
|
|
async def protected(request):
|
|
if request.method != "POST":
|
|
return JsonResponse(
|
|
{"detail": "Method not allowed."},
|
|
status=405,
|
|
)
|
|
|
|
try:
|
|
body = json.loads(request.body)
|
|
solution = body["solution"]
|
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
return JsonResponse(
|
|
{"detail": "Invalid request body."},
|
|
status=400,
|
|
)
|
|
|
|
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
|
|
client_ip = request.META["REMOTE_ADDR"]
|
|
|
|
is_valid = await duckity.validate(
|
|
solution,
|
|
client_ip,
|
|
settings.DUCKITY_APPLICATION_SECRET,
|
|
settings.DUCKITY_PROTECTION_PROFILE_ID,
|
|
)
|
|
|
|
if not is_valid:
|
|
return JsonResponse(
|
|
{"detail": "The provided solution token was invalid."},
|
|
status=400,
|
|
)
|
|
|
|
return JsonResponse({
|
|
"message": "This is protected!"
|
|
})
|
|
```
|
|
|
|
### Django Ninja
|
|
|
|
```py lineNumbers
|
|
from ninja import NinjaAPI, Schema
|
|
from ninja.errors import HttpError
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
api = NinjaAPI()
|
|
|
|
|
|
class RequestBodySchema(Schema):
|
|
solution: str
|
|
|
|
|
|
@api.post("/protected")
|
|
async def protected(request, payload: RequestBodySchema):
|
|
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
|
|
client_ip = request.META["REMOTE_ADDR"]
|
|
|
|
is_valid = await duckity.validate(
|
|
payload.solution,
|
|
client_ip,
|
|
settings.DUCKITY_APPLICATION_SECRET,
|
|
settings.DUCKITY_PROTECTION_PROFILE_ID,
|
|
)
|
|
|
|
if not is_valid:
|
|
raise HttpError(400, "The provided solution token was invalid.")
|
|
|
|
return {
|
|
"message": "This is protected!"
|
|
}
|
|
```
|
|
|
|
### Django Rest Framework
|
|
|
|
```py lineNumbers
|
|
import duckity
|
|
|
|
from django.conf import settings
|
|
|
|
from rest_framework import serializers, status
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
|
|
|
|
class RequestBodySerializer(serializers.Serializer):
|
|
solution = serializers.CharField()
|
|
|
|
|
|
class ProtectedView(APIView):
|
|
async def post(self, request):
|
|
serializer = RequestBodySerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
|
|
client_ip = request.META["REMOTE_ADDR"]
|
|
|
|
is_valid = await duckity.validate(
|
|
serializer.validated_data["solution"],
|
|
client_ip,
|
|
settings.DUCKITY_APPLICATION_SECRET,
|
|
settings.DUCKITY_PROTECTION_PROFILE_ID,
|
|
)
|
|
|
|
if not is_valid:
|
|
return Response(
|
|
{"detail": "The provided solution token was invalid."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
return Response({"message": "This is protected!"})
|
|
```
|
|
|
|
### Strawberry
|
|
|
|
```py lineNumbers
|
|
import os
|
|
|
|
import dotenv
|
|
|
|
import duckity
|
|
|
|
import strawberry
|
|
|
|
|
|
dotenv.load_dotenv(override=True)
|
|
|
|
|
|
PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
|
|
APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
|
|
|
|
|
|
@strawberry.type
|
|
class Query:
|
|
pass
|
|
|
|
|
|
@strawberry.type
|
|
class ProtectedResource:
|
|
message: str
|
|
|
|
|
|
@strawberry.type
|
|
class Mutation:
|
|
@strawberry.mutation
|
|
async def protected(self, info: strawberry.Info, solution: str) -> ProtectedResource:
|
|
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
|
|
client_ip = info.context["request"].META["REMOTE_ADDR"]
|
|
|
|
is_valid = await duckity.validate(
|
|
solution,
|
|
client_ip,
|
|
DUCKITY_APPLICATION_SECRET,
|
|
DUCKITY_PROTECTION_PROFILE_ID,
|
|
)
|
|
|
|
if not is_valid:
|
|
raise Exception("The provided solution was not valid.")
|
|
|
|
return ProtectedResource(message="This is protected!")
|
|
|
|
|
|
schema = strawberry.Schema(query=Query, mutation=Mutation)
|
|
```
|
|
|
|
## Contributing & License
|
|
|
|
All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
|
|
documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
|
|
make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
|
|
|
|
This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.
|