Framework helpers¶
The ready-made routes described in Frameworks, by
signature. Importing socialchimp never imports any of these - you only pay
for the one you use.
Django¶
urls
¶
urls(
sc: SocialChimp,
*,
redirect_uri: str,
memory: LoginMemory | None = None,
scopes: Mapping[str, Sequence[str]] | None = None,
secrets: Mapping[str, str] | None = None,
setup_tokens: Mapping[str, str] | None = None,
deliver: DeliverUpdate | None = None,
) -> list[UrlPattern]
Build the routes for signing in and receiving updates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sc
|
SocialChimp
|
The client to work through. |
required |
redirect_uri
|
str
|
Where networks send people back to. |
required |
memory
|
LoginMemory | None
|
Where a half-finished sign-in waits. Left out, one that
lives in this process is used - fine to try things out with,
wrong in production, because two workers do not share it. See
|
None
|
scopes
|
Mapping[str, Sequence[str]] | None
|
Permissions to ask each network for, by network name. |
None
|
secrets
|
Mapping[str, str] | None
|
The secret each network signs its webhooks with, by network name. |
None
|
setup_tokens
|
Mapping[str, str] | None
|
The token each network's setup check quotes back, by network name. |
None
|
deliver
|
DeliverUpdate | None
|
Where a webhook's update goes. |
None
|
Returns:
| Type | Description |
|---|---|
list[UrlPattern]
|
Patterns to give |
list[UrlPattern]
|
|
Source code in src/socialchimp/contrib/django.py
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
get_client
cached
¶
get_client() -> SocialChimp
Return the one SocialChimp for this process, built from settings.
Reads settings.SOCIALCHIMP, which names your storage class and says
which sort it is:
SOCIALCHIMP = {"SYNC_STORAGE": "myapp.social.MyStorage"}
Use SYNC_STORAGE for a class written as ordinary Django ORM code -
which is what you want unless you have gone out of your way - and
STORAGE for one whose five methods are already async. Exactly one of
them, because guessing which you meant is the sort of thing that works
until it does not.
The client is built once and kept, because the locks that stop two
workers renewing the same token at once live on it. Call
get_client.cache_clear() if you really need a new one.
Returns:
| Type | Description |
|---|---|
SocialChimp
|
The client. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If the setting is missing, the wrong shape, or names a class that is not there. |
Source code in src/socialchimp/contrib/django.py
orm_storage
¶
orm_storage(inner: SyncStorage) -> Storage
Let socialchimp use storage you wrote as ordinary Django ORM code.
Write the five methods with Model.objects.get(...) and .save(), the
way you write everything else, and hand the class here.
Example
class MyStorage: def get_connection(self, connection_id): row = SocialAccount.objects.filter(pk=connection_id).first() return row.to_connection() if row else None ...
sc = SocialChimp(storage=orm_storage(MyStorage()))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inner
|
SyncStorage
|
Your storage class. Five methods, none of them async. |
required |
Returns:
| Type | Description |
|---|---|
Storage
|
A |
Source code in src/socialchimp/contrib/django.py
Request
¶
Bases: Protocol
The little of Django's request these routes read.
Written down here because Django ships no type information, and because
it is a short and useful list: the method, the raw body, the query
values and the headers. A real HttpRequest has all four.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
|
body |
bytes
|
The request body, exactly as it arrived. This is the one that matters for webhooks. |
GET |
Mapping[str, str]
|
The query values. |
headers |
Mapping[str, str]
|
The request headers. |
View
¶
Bases: Protocol
One of the views below, as Django will call it.
MakeResponse
¶
Bases: Protocol
Django's HttpResponse, as much of it as we use.
MakePath
¶
Bases: Protocol
Django's path, as much of it as we use.
Exempt
¶
Bases: Protocol
Django's csrf_exempt.
Settings
¶
Bases: Protocol
Django's settings, which we only ever read one name out of.
FastAPI¶
router
¶
router(
sc: SocialChimp,
*,
redirect_uri: str,
memory: LoginMemory | None = None,
scopes: Mapping[str, Sequence[str]] | None = None,
secrets: Mapping[str, str] | None = None,
setup_tokens: Mapping[str, str] | None = None,
deliver: DeliverUpdate | None = None,
) -> APIRouter
Build the routes for signing in and receiving updates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sc
|
SocialChimp
|
The client to work through. Keep one for the life of your process, and hand the same one to your own code. |
required |
redirect_uri
|
str
|
Where networks send people back to. |
required |
memory
|
LoginMemory | None
|
Where a half-finished sign-in waits. Left out, one that
lives in this process is used - fine to try things out with,
wrong in production. See |
None
|
scopes
|
Mapping[str, Sequence[str]] | None
|
Permissions to ask each network for, by network name. |
None
|
secrets
|
Mapping[str, str] | None
|
The secret each network signs its webhooks with, by network name. |
None
|
setup_tokens
|
Mapping[str, str] | None
|
The token each network's setup check quotes back, by network name. |
None
|
deliver
|
DeliverUpdate | None
|
Where a webhook's update goes. |
None
|
Returns:
| Type | Description |
|---|---|
APIRouter
|
A router to give |
Source code in src/socialchimp/contrib/fastapi.py
Flask¶
blueprint
¶
blueprint(
sc: SocialChimp,
*,
redirect_uri: str,
memory: LoginMemory | None = None,
scopes: Mapping[str, Sequence[str]] | None = None,
secrets: Mapping[str, str] | None = None,
setup_tokens: Mapping[str, str] | None = None,
deliver: DeliverUpdate | None = None,
name: str = "socialchimp",
) -> Blueprint
Build the routes for signing in and receiving updates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sc
|
SocialChimp
|
The client to work through. Keep one for the life of your process, and hand the same one to your own code. |
required |
redirect_uri
|
str
|
Where networks send people back to. |
required |
memory
|
LoginMemory | None
|
Where a half-finished sign-in waits. Left out, one that
lives in this process is used - fine to try things out with,
wrong in production. See |
None
|
scopes
|
Mapping[str, Sequence[str]] | None
|
Permissions to ask each network for, by network name. |
None
|
secrets
|
Mapping[str, str] | None
|
The secret each network signs its webhooks with, by network name. |
None
|
setup_tokens
|
Mapping[str, str] | None
|
The token each network's setup check quotes back, by network name. |
None
|
deliver
|
DeliverUpdate | None
|
Where a webhook's update goes. |
None
|
name
|
str
|
What to call the blueprint. Change it if you register two. |
'socialchimp'
|
Returns:
| Type | Description |
|---|---|
Blueprint
|
A blueprint to give |
Source code in src/socialchimp/contrib/flask.py
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
run
¶
Run one async call from Flask's thread and wait for the answer.
The routes below use this, and so should your own views - it is the same bridge, using the same loop, so the connections socialchimp pools are shared with the routes rather than thrown away after every call.
@app.post("/posts")
def write():
account = sc.account(request.form["connection_id"])
result = run(account.post(Post(text=request.form["text"])))
return {"id": result.id, "url": result.url}
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
work
|
Coroutine[Any, Any, T]
|
The call to run. |
required |
Returns:
| Type | Description |
|---|---|
T
|
What it answered. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever the call raised, raised again here. |
Source code in src/socialchimp/contrib/flask.py
Shared by all three¶
Nothing here knows what a request object looks like or imports any framework. Each framework's file takes a request apart into plain values, calls something here, and turns the result back into that framework's own response.
Routes
¶
Routes(
sc: SocialChimp,
*,
redirect_uri: str,
memory: LoginMemory | None = None,
scopes: Mapping[str, Sequence[str]] | None = None,
secrets: Mapping[str, str] | None = None,
setup_tokens: Mapping[str, str] | None = None,
deliver: DeliverUpdate | None = None,
)
Signing in and receiving a webhook, with no framework in sight.
Each method takes plain values - a network's name, a mapping of query
values, the raw bytes of a body - and hands back a Reply. A framework
file does the taking apart and the putting back together, and nothing
else.
Every method is a wrapper around a SocialChimp method you could call
yourself. Anything the caller did wrong, and anything a network said no
to, comes back as a Reply with a sensible status, so a route never has
to catch those. Two things are raised instead, because both are yours to
deal with and neither is the caller's fault:
ConfigError. Something is set up wrong - a secret that was never stored, an app that was never registered. It would be the same mistake on every request, so answering a tidy 500 only buries it in a log. Raised, it stops you in development and shows up as an error in production, which is what a mistake in your own set-up deserves.- Whatever
deliverraised - anExceptionGroupof the handlers that failed, if it isDispatcher.deliver. Seewebhook.
Example
routes = Routes(sc, redirect_uri="https://app.example/cb/{platform}") reply = await routes.start("mastodon", {"host": "mastodon.social"})
Say how these routes should behave.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sc
|
SocialChimp
|
The client to work through. Keep one for the life of your
process - see |
required |
redirect_uri
|
str
|
Where networks send people back to. |
required |
memory
|
LoginMemory | None
|
Where a half-finished sign-in waits. Left out, one that
lives in this process is used, which is fine to try things
out with and wrong in production - see |
None
|
scopes
|
Mapping[str, Sequence[str]] | None
|
Permissions to ask each network for, by network name. Anything not named here uses that platform's own defaults. |
None
|
secrets
|
Mapping[str, str] | None
|
The secret each network signs its webhooks with, by network name. Meta calls this the app secret. |
None
|
setup_tokens
|
Mapping[str, str] | None
|
The token each network's setup check quotes back, by network name. Meta's forms call this the verify token. |
None
|
deliver
|
DeliverUpdate | None
|
Where a webhook's update goes. |
None
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
If there are webhook secrets but no |
Source code in src/socialchimp/contrib/shared.py
start
async
¶
start(platform: str, params: Mapping[str, str]) -> Reply
Begin signing someone in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
str
|
Which network, for example |
required |
params
|
Mapping[str, str]
|
The query values. |
required |
Returns:
| Type | Description |
|---|---|
Reply
|
A redirect to the network for most networks. For a network |
Reply
|
signed in to with an app password or a bot token, the fields to |
Reply
|
show a person, as JSON. |
Source code in src/socialchimp/contrib/shared.py
finish
async
¶
finish(platform: str, params: Mapping[str, str]) -> Reply
Carry on after the person comes back from the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
str
|
Which network. |
required |
params
|
Mapping[str, str]
|
The query values the network sent back, or - for a
network that asked for details instead - what the person
typed. Either way it has to carry the same |
required |
Returns:
| Type | Description |
|---|---|
Reply
|
The connected account as JSON, or the accounts to choose |
Reply
|
between when the network needs to know which page or channel to |
Reply
|
use. |
Source code in src/socialchimp/contrib/shared.py
choose
async
¶
choose(platform: str, params: Mapping[str, str]) -> Reply
Carry on a sign-in after the person picked which account to use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
str
|
Which network. |
required |
params
|
Mapping[str, str]
|
|
required |
Returns:
| Type | Description |
|---|---|
Reply
|
The connected account as JSON. |
Source code in src/socialchimp/contrib/shared.py
webhook
async
¶
webhook(
platform: str, body: bytes, headers: Mapping[str, str]
) -> Reply
Receive one request a network pushed to us.
The body must be the raw bytes of the request, exactly as they
arrived. A signature is over those exact bytes, so a framework that
parses the JSON and builds it again has already broken it - the
spacing and the key order will not match. Read the body, pass it
here, and let read_update do the parsing afterwards. This is the
single most common reason a correct signature appears to fail.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
str
|
Which network. |
required |
body
|
bytes
|
The request body, untouched. |
required |
headers
|
Mapping[str, str]
|
The request headers. |
required |
Returns:
| Type | Description |
|---|---|
Reply
|
200 when the request was signed properly and every update in it |
Reply
|
was handed on. 401 when it was not signed properly, with nothing |
Reply
|
said about which check failed. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If these routes are not set up to receive this network's webhooks, or the platform file is wrong about itself. Both are mistakes to fix rather than answers to send. |
Exception
|
Whatever |
Source code in src/socialchimp/contrib/shared.py
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 | |
setup_check
async
¶
setup_check(
platform: str, params: Mapping[str, str]
) -> Reply
Answer the one-off check a network makes before it will send us anything.
Meta does a GET at the same address with a token you chose and a challenge to echo back. Get it wrong and it says the URL could not be verified, without saying why.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
str
|
Which network. |
required |
params
|
Mapping[str, str]
|
The query values from the check. |
required |
Returns:
| Type | Description |
|---|---|
Reply
|
The challenge as plain text, or 403 if the token was not ours. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If no setup token is stored for this network. See the class docstring for why that is raised and not answered. |
Source code in src/socialchimp/contrib/shared.py
Reply
dataclass
¶
Reply(
status: int,
body: bytes,
content_type: str = "application/json",
headers: Mapping[str, str] = dict(),
)
What a route decided to answer, before any framework is involved.
Plain bytes and a status, so the same decision can become a FastAPI
Response, a Flask one or a Django one without being decided three
times.
Attributes:
| Name | Type | Description |
|---|---|---|
status |
int
|
The HTTP status code. |
body |
bytes
|
Exactly what to send, already encoded. |
content_type |
str
|
What to say the body is. |
headers |
Mapping[str, str]
|
Anything else to send, such as where to redirect to. |
json
classmethod
¶
json(
data: Mapping[str, object], *, status: int = 200
) -> Reply
Answer with a JSON object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Mapping[str, object]
|
What to send. |
required |
status
|
int
|
The status code. |
200
|
Returns:
| Type | Description |
|---|---|
Reply
|
The reply. |
Source code in src/socialchimp/contrib/shared.py
text
classmethod
¶
text(words: str, *, status: int = 200) -> Reply
Answer with plain text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
words
|
str
|
What to send. |
required |
status
|
int
|
The status code. |
200
|
Returns:
| Type | Description |
|---|---|
Reply
|
The reply. |
Source code in src/socialchimp/contrib/shared.py
redirect
classmethod
¶
redirect(url: str) -> Reply
Send the person's browser somewhere else.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
Where to send them. |
required |
Returns:
| Type | Description |
|---|---|
Reply
|
The reply. |
Source code in src/socialchimp/contrib/shared.py
for_error
classmethod
¶
for_error(error: SocialChimpError) -> Reply
Turn one of our errors into an answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
error
|
SocialChimpError
|
What went wrong. |
required |
Returns:
| Type | Description |
|---|---|
Reply
|
The reply, with the status |
Source code in src/socialchimp/contrib/shared.py
status_for
¶
status_for(error: SocialChimpError) -> int
Return the status code that fits one of our errors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
error
|
SocialChimpError
|
What went wrong. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The status to answer with. Anything we have no particular answer for |
int
|
is 500, on the basis that an error we did not plan for is our |
int
|
problem and not the caller's. |
Source code in src/socialchimp/contrib/shared.py
read_form
¶
Read the values out of a form's body.
Used instead of each framework's own form parsing, so that all three behave identically and none of them needs an extra package installed to read an ordinary HTML form.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The raw body of a form post. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
The values, by name. |
Source code in src/socialchimp/contrib/shared.py
LoginMemory
¶
Bases: Protocol
Where a half-finished sign-in waits for the person to come back.
Signing in is two requests. The first one is handed something the second
one needs - the secret half of a PKCE pair, which server the person
named, and later the resume token from ChooseAccount. socialchimp
cannot keep any of that for you: the person can be sent away by one web
worker and come back to another, so anything held in one process works
on your laptop and fails in production.
Everything is filed under the sign-in's state, which is the one value
that makes the round trip through the network.
Back this with whatever your app already has - a session, a Redis key
with a short life, a small table. InMemoryLoginMemory is here to try
things out with.
keep
async
¶
Write down what the rest of this sign-in will need.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The sign-in's state, which is the key. |
required |
data
|
RawData
|
What to keep. Plain JSON-shaped data. |
required |
look_up
async
¶
Read back what was kept for one sign-in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The sign-in's state. |
required |
Returns:
| Type | Description |
|---|---|
RawData | None
|
What was kept, or |
Source code in src/socialchimp/contrib/shared.py
forget
async
¶
Throw away one sign-in's notes. Quiet if there are none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The sign-in's state. |
required |
InMemoryLoginMemory
¶
A memory that lives in one process and is lost on restart.
Fine for trying things out and for tests. Not fine in production: two web workers do not share it, so a person sent away by one and returning to another is told their sign-in has expired, and every restart loses every sign-in in flight.
Use your session, or a Redis key, or a small table instead.
What is kept is capped, so that abandoned sign-ins cannot fill up the process. Once it is full the oldest are forgotten first.
Start with nothing remembered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_size
|
int
|
How many half-finished sign-ins to hold before forgetting the oldest. |
_DEFAULT_MEMORY_SIZE
|
Source code in src/socialchimp/contrib/shared.py
keep
async
¶
Write down what the rest of this sign-in will need.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The sign-in's state, which is the key. |
required |
data
|
RawData
|
What to keep. |
required |
Source code in src/socialchimp/contrib/shared.py
look_up
async
¶
Read back what was kept for one sign-in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The sign-in's state. |
required |
Returns:
| Type | Description |
|---|---|
RawData | None
|
What was kept, or |
forget
async
¶
Throw away one sign-in's notes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The sign-in's state. |
required |