Python API wrapper
Introduction
This library provides an SDK for the Transifex API which is located at the transifex.api
package of the transifex-python library in PyPI.
It is based on a low-level library for building SDKs for {json:api} APIs located at the transifex.api.jsonapi
package. If you understand how the low-level {json:api}
library maps to HTTP interactions and consult the Transifex API documentation, you should be able to make use of the SDK.
This document is split between two sections: one that provides an overview of how to work with the Transifex API and one that shows how you can use transifex.api.jsonapi
to build an SDK that can interact with any API that follows the {json:api}
. You should read the second part if you want to troubleshoot or understand how the internals of the SDK work.
transifex.api
usage
transifex.api
usageSetting up
from transifex.api import transifex_api
transifex_api.setup(auth="...")
The auth
argument should be an API token. You can generate one at https://www.transifex.com/user/settings/api/.
Finding things
To get a list of the organizations your user account has access to, run:
transifex_api.Organization.list()
If you have access to many organizations and the first response comes paginated, you can get a list of all organizations with:
# .all returns a generator
list(transifex_api.Organization.all())
It is highly unlikely that you will have access to so many organizations for the initial response to be paginated but the list
and all
methods are common to all Transifex API resource types so you might as well get used to them. If the list fits into one response, using all
instead of list
doesn't have any penalties.
If you want to find a specific organization, you can use the slug
filter:
organization = transifex_api.Organization.filter(slug="my_org")[0]
# or
organization = transifex_api.Organization.get(slug="my_org")
get
does the same thing asfilter(...)[0]
but raises an exception if the number of results is not 1.
Alternatively (if for example you don't know the slug but the name of the organization), you can search against all of them:
organization = None
for o in transifex_api.Organization.all():
if o.name == "My Org":
organization = o
break
After you get an Organization
instance, you can access its attributes:
organization.name
# <<< 'My organization'
To get a list of projects, do:
projects = transifex_api.Project.filter(organization=organization)
However, if you look at how a project is represented in the Get project details, Organization objects have a projects
relationship with a related
links, so you can achieve the same thing with:
projects = organization.fetch('projects')
If you look into the List projects, you can see that a slug
filter is also supported, so to find a specific project, you can do:
project = organization.fetch('projects').get(slug="my_project")
Projects also have a languages
relationship. This means that you can access a project's target languages with:
languages = project.fetch('languages')
Changing attributes
Let's use what we've learned so far alongside the API documentation to find a "untranslated string slot" (the /resource_translations
endpoint returns items for strings that haven't been translated yet, setting their strings
field will post a translation):
language = transifex_api.Language.get(code="el")
resource = project.fetch('resources').get(slug="my_resource")
translations = transifex_api.ResourceTranslation.\
filter(resource=resource, language=language).\
include('resource_string')
translation = translations[0]
Appending a
.include
to a filter will pre-fetch a relationship. In the case of ResourceTranslation, this will also fetch the source string information for the "translation slot". Again, you should consult the API documentation to see if including relationships is supported for a given API resource type.
In order to save a translation to the server, we use .save
:
# We don't have to fetch the resource string because it has been included
source_string = translation.resource_string.strings['other']
translation.strings = {'other': source_string + " in greeeek!!!"}
translation.save('strings')
We have to specify which fields we will be sending to the API with save
's positional arguments.
Because this is a common use-case (setting attributes and immediately saving them on the server), there is a shortcut:
translation.save(strings={'other': source_string + " in greeek!!!"})
Changing relationships
Lets use projects, teams and project languages as examples:
project = transifex_api.Project.get(organization=..., slug="...")
team_1 = project.fetch('team')
team_2 = transifex_api.Team.get(slug="...")
If we want to change the project's team from team_1
to team_2
, we have 2 options:
project.team = team_2
project.save('team')
# Or
project.save(team=team_2)
This is similar to how we change attributes. The other option is:
project.change('team', team_2)
This will send a PATCH request to /projects/XXX/relationships/team
to perform the change. Again, you should consult the API documentation to see
which relationships can be changed and with which methods (in this case -
changing a project's team - both methods are available).
The project -> team
is a "singular relationship" (singular relationships are either one-to-one or foreign-key relationships). To change a "plural relationship", like a project's target languages, you can use the reset
, add
and remove
methods:
language_dict = {
language.code: language
for language in transifex_api.Language.all()
}
language_a, language_b, language_c = language_dict['a'], language_dict['b'], language_dict['c']
# This will completely replace the project's target languages
# The project's languages after this will be: ['a', 'b']
project.reset('languages', [language_a, language_b])
# This will append the supplied languages to the project's target languages
# The project's languages after this will be: ['a', 'b', 'c']
project.add('languages', [language_c])
# This will remove the supplied languages from the project's target languages
# The project's languages after this will be: ['a', 'c']
project.remove('languages', [language_b])
The HTTP methods used for reset
, add
and remove
are PATCH
, POST
and DELETE
respectively. As always, you should consult the API documentation to see if the relationship in question is editable and which methods are supported.
Creating and deleting things
The following examples should be self-explanatory.
To create something:
organization = transifex_api.Organization.list()[0]
source_language = transifex_api.Language.list()[0]
project = transifex_api.Project.create(name="New Project",
slug="new_project",
private=True,
organization=organization,
source_language=source_language)
You can see which fields are supported in the API documentation. The organization
and source_language
arguments are interpreted as relationships.
To delete something:
project.delete()
File uploads and downloads
There is code in transifex.api
that automates several {json:api} interactions behind the scenes in order to help with file uploads and downloads.
In order to upload a source file to a resource, you can do:
resource = transifex_api.Resource.filter(...)[0]
content = "The new source file content"
transifex_api.ResourceStringsAsyncUpload.upload(resource, content)
In order to download a translated language file, you can do:
language = transifex_api.Language.list()[0]
url = transifex_api.ResourceTranslationsAsyncDownload.\
download(resource=resource, language=language)
translated_content = requests.get(url).text
As always, in order to see how file uploads and downloads work in the Transifex API, you should check out the API documentation.
transifex.api.jsonapi
usage
transifex.api.jsonapi
usageSetting up
Using transifex.api.jsonapi
means creating your own API SDK for a remote service. In order to do that, you need to first define an API connection type. This is done by subclassing transifex.api.jsonapi.JsonApi
:
from transifex.api.jsonapi import JsonApi
class FamilyApi(JsonApi):
HOST = "https://api.families.com"
Next, you have to define some API resource types and register them to the API connection type. This is done by subclassing transifex.api.jsonapi.Resource
and decorating it with the connection type's register
method:
from transifex.api.jsonapi import Resource
@FamilyApi.register
class Parent(Resource):
TYPE = "parents"
@FamilyApi.register
class Child(Resource):
TYPE = "children"
Users of your SDK can then instantiate your API connection type, providing authentication credentials and/or overriding the host, in case you want to test against a sandbox API server and not the production one:
family_api = FamilyApi(host="https://sandbox.api.families.com",
auth="<MY_TOKEN>")
Finally the API resource types you have registered can be accessed as attributes on this API connection instance. You can either use the class's name or the API resource's type:
child = family_api.Child.get('1')
child = family_api.children.get('1')
This is enough to get you started since the library will be able to provide you with a lot of functionality based on the structure of the responses you get from the server. Make sure you define and register Resource subclasses for every type you intend to encounter, because transifex.api.jsonapi
will use the API instance's registry to resolve the appropriate subclass for the items included in the API's responses.
Global API connection instances
You can configure an already created API connection instance by calling the setup
method, which accepts the same keyword arguments as the constructor. In fact, JsonApi
's __init__
and setup
methods have been written in such a way that the following two snippets should produce an identical outcome:
kwargs = ...
family_api = FamilyApi(**kwargs)
kwargs = ...
family_api = FamilyApi()
family_api.setup(**kwargs)
This way, you can implement your SDK in a way that offers the option to users to either use a global API connection instance or multiple instances. In fact, this is exactly how transifex.api
has been set up:
# src/transifex.api/__init__.py
from transifex.api.jsonapi import JsonApi, Resource
class TransifexApi(JsonApi):
HOST = "https://rest.api.transifex.com"
@TransifexApi.register
class Organization(Resource):
TYPE = "organizations"
transifex_api = TransifexApi()
# app.py (uses the global API connection instance)
from transifex.api import transifex_api
transifex_api.setup(auth="<API_TOKEN>")
organization = transifex_api.Organization.get("1")
# app.py (uses multiple custom API connection instances)
from transifex.api import TransifexApi
api_1 = TransifexApi(auth="<API_TOKEN_1>")
api_2 = TransifexApi(auth="<API_TOKEN_2>")
organization_1 = api_1.Organization.get("1")
organization_2 = api_2.Organization.get("2")
The whole logic behind this initialization process is further explained
here
Authentication
The auth
argument to JsonApi
or setup
can either be:
- A string, in which case all requests to the API server will include the
Authorization: Bearer <API_TOKEN>
header - A callable, in which case the return value is expected to be a dictionary which will be merged with the headers of all requests to the API server
import datetime
from family_api import FamilyApi
from .secrets import KEY
from .crypto import sign
def myauth():
return {'x-signature': sign(KEY, datetime.datetime.now())}
family_api = FamilyApi(auth=myauth)
Custom headers
You can supply custom HTTP headers to be sent with every request to the remote server using the headers
keyword argument to the JsonApi
constructor or the setup
method.
from family_api import FamilyApi
family_api = FamilyApi(..., headers={'X-Application': "My-client"})
Retrieval
URLs
By default, collection URLs have the form /<type>
(eg /children
) and item URLs have the form /<type>/<id>
(eg /children/1
). This is also part of {json:api}'s recommendations. If you want to customize them, you need to override the get_collection_url
classmethod and the get_item_url()
method of the resource's subclass:
@FamilyApi.register
class Child(Resource):
TYPE = "children"
@classmethod
def get_collection_url(cls):
return "/children_collection"
def get_item_url(self):
return f"/child_item/{self.id}"
Getting a single resource object from the API
If you know the ID of the resource object, you can fetch its {json:api} representation with:
child = family_api.Child.get("1")
The attributes of a resource object are id
, attributes
, relationships
, links
and related
. id
, attributes
, relationships
and links
have exactly the same value as in the API response.
parent = family_api.Parent.get("1")
parent.id
# "1"
parent.attributes
# {'name': "Zeus"}
parent.relationships
# {'children': {'links': {'self': "/parent/1/relationships/children",
# 'related': "/children?filter[parent]=1"}}}
child = family_api.Child.get("1")
child.id
# "1"
child.attributes
# {'name': "Hercules"}
child.relationships
# {'parent': {'data': {'type': "parents", 'id': "1"},
# 'links': {'self': "/children/1/relationships/parent",
# 'related': "/parents/1"}}}
You can reload an object from the server by calling .reload()
:
child.reload()
# equivalent to
child = family_api.Child.get(child.id)
Relationships
Intro
We need to talk a bit about how {json:api} represents relationships and how the transifex.api.jsonapi
library interprets them. Depending on the value of a field of relationships
, we consider the following possibilities. A relationship can either be:
-
A null relationship which will be represented by a null value:
{'type': "children", 'id': "...", 'attributes': { ... }, 'relationships': { 'parent': null, # <--- ..., }, 'links': { ... }}
-
A singular relationship which will be represented by an object with both
data
andlinks
fields, with thedata
field being a dictionary:{'type': "children", 'id': "...", 'attributes': { ... }, 'relationships': { 'parent': {'data': {'type': "parents", 'id': "..."}, # <--- 'links': {'self': "...", 'related': "..."}}, # <--- ... , }, 'links': { ... }}
-
A plural relationship which will be represented by an object with a
links
field and either a missingdata
field or adata
field which is a
list:{'type': "parents", 'id': "...", 'attributes': { ... }, 'relationships': { 'children': {'links': {'self': "...", 'related': "..."}}, # <--- ..., }, 'links': { ... }}
or
{'type': "parents", 'id': "...", 'attributes': { ... }, 'relationships': { 'children': {'links': {'self': "...", 'related': "..."}, # <--- 'data': [{'type': "children", 'id': "..."}, # <--- {'type': "children", 'id': "..."}, # <--- ... ]}, # <--- ... , }, 'links': { ... }}
This is important because transifex.api.jsonapi
will make assumptions about the nature of relationships based on the existence of these fields.
Fetching relationships
The related
field is meant to host the data of the relationships, after these have been fetched from the API. Lets revisit the last example and inspect the relationships
and related
fields:
parent = family_api.Parent.get("1")
parent.relationships
# {'children': {'links': {'self': "/parent/1/relationships/children",
# 'related': "/children?filter[parent]=1"}}}
parent.related
# {}
child = family_api.Child.get("1")
child.relationships
# {'parent': {'data': {'type': "parents", 'id': "1"},
# 'links': {'self': "/children/1/relationships/parent",
# 'related': "/parents/1"}}}
child.related
# {parent: <Parent: 1 (Unfetched)>}
As you can see, the parent→children related
field is empty while the child→parent related
field is prefilled with an "unfetched" Parent instance. This happens becaue the first one is a plural relationship while the second is a singular relationship. Unfetched means that we only know its id
so far. In both cases, we don't know any meaningful data about the relationships yet.
In order to fetch the related data, you need to call .fetch()
with the names of the relationships you want to fetch:
child.related
# {'parent': <Parent: 1 (Unfetched)>}
(child.related['parent'].id,
child.related['parent'].attributes,
child.related['parent'].relationships)
# ("1", {}, {})
child.fetch('parent') # Now `related['parent']` has all the information
child.related
# {parent: <Parent: 1>}
(child.related['parent'].id,
child.related['parent'].attributes,
child.related['parent'].relationships)
# ("1",
# {'name': "Zeus"},
# {'children': {'links': {'self': "/parent/1/relationships/children",
# 'related': "/children?filter[parent]=1"}}})
parent.fetch('children')
parent.related
# {'children': [<Child: 1>, <Child: 2>]}
(parent.related['children'][0].id,
parent.related['children'][0].attributes,
parent.related['children'][0].relationships)
# ("1",
# {'name': "Hercules"},
# {'parent': {'data': {'type': "parents", 'id': "1"},
# 'links': {'self': "/children/1/relationships/parent",
# '/parents/1'}}})
Trying to fetch an already-fetched relationship will not actually trigger
another request, unless you pass force=True
to .fetch()
.
If .fetch()
is only provided with one positional argument, it will return the
relation:
parent = family_api.Parent.get("1")
print(parent.fetch('children')[1].name)
# "Hercules"
# Is equivalent to:
parent.fetch('children')
print(parent.related['children'][1].name)
Shortcuts
You can access all keys in attributes
and related
directly on the resource
object:
child.name == child.attributes['name'] == "Hercules"
# True
This is very handy, both for reading and setting values to those fields,
however you should be careful when setting them. If the key is not already part
of attributes
or relationships
, the assignment will fall back to the
default operation of Python objects, which is to add the key to the __dict__
attribute:
child.__dict__
# {'id': ..., 'attributes': {'name': "Hercules"}, ...}
child.name = "Achilles"
child.__dict__
# {'id': ..., 'attributes': {'name': "Achilles"}, ...}
# ^^^^^^^^^^
child.hair_color = "red"
child.__dict__
# {'id': ..., 'attributes': {'name': "Achilles"}, 'hair_color': "red", ...}
# ^^^^^^^^^^^^^^^^^^^
Be careful of this because the new keys will not be included in subsequent
PATCH operations to update the resource on the server. Normally you won't have
to worry about this since the API server will likely have provided all
attributes and relationships it is likely to accept in subsequent requests,
even if their value is set to null
. If you definitely want to add a new field
to an object's attributes
or relationships
, you can always fall back to
doing so directly:
child.attributes['hair_color'] = "red"
child.__dict__
# {'id': ..., 'attributes': {'name': "Hercules", 'hair_color': "red"}, ...}
# ^^^^^^^^^^^^^^^^^^^
Getting Resource collections
You can access a collection of resource objects using one of the list
,
filter
, page
, include
,sort
, fields
, extra
, all
and all_pages
classmethods of Resource subclass.
children = family_api.Child.list()
# [<Child: 1>, <Child: 2>, ...]
Each method does the following:
-
list
returns the first page of the results -
filter
applies filters; nested filters are separated by double underscores
(__
), Django-styleoperation GET request .filter(a=1)
?filter[a]=1
.filter(a__b=1)
?filter[a][b]=1
Note: because it's a common use-case, using a resource object as the value
of a filter operation will result in using itsid
fieldparent = family_api.Parent.get("1") family_api.Child.filter(parent=parent) # is equivalent to family_api.Child.filter(parent=parent.id)
-
page
applies pagination; it accepts either one positional argument which
will be passed to thepage
GET parameter or multiple keyword arguments
which will be passed as nestedpage
GET parametersoperation GET request .page(1)
?page=1
.page(a=1, b=2)
?page[a]=1&page[b]=2
(Note: you will probably not have to use
.page
yourself since the returned
lists support pagination on their own, see below) -
include
will set theinclude
GET parameter; it accepts multiple
positional arguments which it will join with commas (,
)operation GET request .include('parent', 'pet')
?include=parent,pet
-
sort
will set thesort
GET parameter; it accepts multiple positional
arguments which it will join with commas (,
)operation GET request .sort('age', 'name')
?sort=age,name
-
fields
will set thefields
GET parameter; it accepts multiple positional
arguments which it will join with commas (,
)operation GET request .fields('age', 'name')
?fields=age,name
-
extra
accepts any keyword arguments which will be added to the GET
parameters sent to the APIoperation GET request .extra(group_by="age")
?group_by=age
-
all
returns a generator that will yield all results of a paginated
collection, using multiple requests if necessary; the pages are fetched
on-demand, so if you abort the generator early, you will not be performing
requests against every possible page -
all_pages
returns a generator of non-empty pages; similarly toall
, pages
are fetched on-demand (in fact,all
usesall_pages
internally)
All the above methods can be chained to each other. So:
family_api.Child.list().filter(a=1)
# is equivalent to
family_api.Child.filter(a=1)
family_api.Child.filter(a=1).filter(b=2)
# is equivalent to
family_api.Child.filter(a=1, b=2)
family_api.Child.list().all()
# is equivalent to
family_api.Child.all()
The collections are also lazy (Django-style). You will not actually make any
requests to the server until you try to access a collection like a list. So
this:
def get_children(gender=None, hair_color=None):
result = family_api.Child.list()
if gender is not None:
result = result.filter(gender=gender)
if hair_color is not None:
result = result.filter(hair_color=hair_color)
return result
print([child.name for child in get_children(hair_color="red")])
will only make one request to the server during the execution of the list
comprehension in the last line.
You can also access pagination via the has_next
, has_previous
, next
and
previous
methods of a returned list (which is what all_pages
and all
use
internally).
All the previous methods also work on plural relationships (assuming the API
supports the applied filters etc on the endpoint specified by the related
link of the relationship).
print(parent.fetch('children').filter(name="Hercules")[0].name)
# Will print the names of the *first page* of the children
print([child.name for child in parent.children])
# Will print the names of the *all* the children
print([child.name for child in parent.children.all()])
Prefetching relationships with include
include
If you use the include
method on a collection retrieval or if you use the
include
keyword argument on .get()
(and if the server supports it), the
included values of the response will be used to prefill the relevant fields of
related
:
child = family_api.Child.get("1", include=['parent'])
child.parent.name # No need to fetch the parent
# "Zeus"
children = family_api.Child.list().include('parent')
[child.parent.name for child in children] # No need to fetch the parents
# ["Zeus", "Zeus", ...]
In case of a plural relationships with a list data
field, if the response
supplies the related items in the included
section, these too will be
prefilled.
parent = family_api.Parent.get("1", include=['children'])
# Assuming the response looks like:
# {'data': {'type': "parents",
# 'id': "1",
# 'attributes': ...,
# 'relationships': {'children': {'data': [{'type': "children", 'id': "1"},
# {'type': "children", 'id': "2"}],
# 'links': ...}}},
# 'included': [{'type': "children",
# 'id': "1",
# 'attributes': {'name': "Hercules"}},
# {'type': "children",
# 'id': "2",
# 'attributes': {'name': "Achilles"}}]}
[child.name for child in parent.children] # No need to fetch
# ["Hercules", "Achilles"]
Getting single resource objects using filters
Appending .get()
to a collection will ensure that the collection is of size 1
and return the one resource instance in it. If the collection's size isn't 1,
it will raise a transifex.api.jsonapi.DoesNotExist
or
transifex.api.jsonapi.MultipleObjectsReturned
exception accordingly (both are
subclasses of transifex.api.jsonapi.NotSingleItem
).
child = family_api.Child.filter(name="Bill").get()
The Resource
's .get()
classmethod, which we covered before, also accepts
keyword arguments, if a positional id
argument isn't used. Calling it this
way, will apply the filters and use the collection's .get()
method on the
result.
child = family_api.Child.get(name="Bill")
# is equivalent to
child = family_api.Child.filter(name="Bill").get()
Note: The Resource
's .get()
classmethod accepts an include
keyword
argument as well, so be careful of naming conflicts if you want to use a filter
called 'include'
# Don't do this
family_api.Child.get(name="Bill", include="parent")
# equivalent to
family_api.Child.filter(name="Bill").include('parent').get()
# Do this instead
child = family_api.Child.filter(name="Bill", include="parent").get()
Editing
Saving changes
After you change some attributes or relationships, you can call .save()
on an
object, which will trigger a PATCH request to the server. Because usually the
server includes immutable fields with the response (creation timestamps etc),
you don't want to include all attributes and relationships in the request. You
can specify which fields will be sent with:
.save()
's positional arguments, or- the
EDITABLE
class attribute of the Resource subclass
child = family_api.Child.get("1")
child.name += " the Great"
child.save('name')
# or
@FamilyApi.register
class Child(Resource):
TYPE = "children"
EDITABLE = ['name']
child = family_api.Child.get("1")
child.name += " the Great"
child.save()
Because setting values right before saving is a common use-case, .save()
also
accepts keyword arguments. These will be set on the resource object, right
before the actual saving:
child.save(name="Hercules")
# is equivalent to
child.name = "Hercules"
child.save('name')
Creating new resources
Calling .save()
on an object whose id
is not set will result in a POST
request which will (attempt to) create the resource on the server.
parent = family_api.Parent.get("1")
child = family_api.Child(attributes={'name': "Hercules"},
relationships={'parent': parent})
child.save()
After saving, the object will have the id
returned by the server, plus any
other server-generated attributes and relationships (for example, creation
timestamps).
There is a shortcut for the above, called .create()
parent = family_api.Parent.get("1")
child = family_api.Child.create(attributes={'name': "Hercules"},
relationships={'parent': parent})
Note: for relationships, you can provide either a resource instance, a
"Resource Identifier" (the 'data' value of a relationship object) or an entire
relationship from another resource. So, the following are equivalent:
# Well, almost equivalent, the first example will trigger a request to fetch
# the parent's data from the server
child = family_api.Child.create(attributes={'name': "Hercules"},
relationships={'parent': family_api.Parent.get("1")})
child = family_api.Child.create(attributes={'name': "Hercules"},
relationships={'parent': family_api.Parent(id="1")})
child = family_api.Child.create(attributes={'name': "Hercules"},
relationships={'parent': {'type': "parents": 'id': "1"}})
child = family_api.Child.create(attributes={'name': "Hercules"},
relationships={'parent': {'data': {'type': "parents": 'id': "1"}}})
This way, you can reuse a relationship from another object when creating,
without having to fetch the relationship:
new_child = family_api.Child.create(attributes={'name': "Achilles"},
relationships={'parent': old_child.parent})
Magic kwargs
When making new (unsaved) instances, or when you create instances on the server
with .create()
, you can supply any keyword argument apart from id
,
attributes
, relationships
, etc and they will be interpreted as attributes
or relationships. Anything that looks like a relationship will be interpreted
as such while everything else will be interpreted as an attribute.
Things that are interpreted as relationships are:
- Resource instances
- Resource identifiers - dictionaries with 'type' and 'id' fields
- Relationship objects - dictionaries with a single 'data' field whose value is
a resource identifier
So
family_api.Child(name="Hercules")
# is equivalent to
family_api.Child(attributes={'name': "Hercules"})
family_api.Child(parent={'type': "parents", 'id': "1"})
# is equivalent to
family_api.Child(relationships={'parent': {'type': "parents", 'id': "1"}})
family_api.Child(parent=family_api.Parent(id="1"))
# is equivalent to
family_api.Child(relationships={'parent': family_api.Parent(id="1")})
If you are worried about naming conflicts, for example if you want to have a
relationship called 'attributes', an attribute that looks like a relationship
and an attribute called 'id', you should fall back to using 'attributes' and
'relationships' directly.
# Don't do this
child = family_api.Child(attributes={'type': "attributes", 'id': "1"},
stats={'type': "stats", 'id': "2"},
id="3")
child.to_dict()
# {'type': "children",
# 'attributes': {'type': "attributes", 'id': "1"},
# 'relationships': {'stats': {'data': {'type': "stats", 'id': "2"}}},
# 'id': "3"}
# Do this instead
child = family_api.Child(relationships={'attributes': {'type': "attributes", 'id': "1"}}
attributes={'stats': {'type': "stats", 'id': "2"}, 'id': "3"})
child.to_dict()
# {'type': "children",
# 'attributes': {'stats': {'type': "stats", 'id': "2"},
# 'id': "3"},
# 'relationships': {'attributes': {'data': {'type': "attributes", 'id': "1"}}}}
Note: .to_dict()
returns the {json:api} representation of the Resource
instance, ie what the payload to the server would be if we called .save()
on
it
Client-generated IDs
Since .save()
will issue a PATCH request when invoked on objects that have an
ID, if you want to supply your own client-generated ID during creation, you
have to use .create()
, which will always issue a POST request.
family_api.Child(attributes={'name': "Hercules"}).save()
# POST: {data: {type: "children", attributes: {name: "Hercules"}}}
family_api.Child(id="1", attributes={'name': "Hercules"}).save()
# PATCH: {data: {type: "children", id: "1", attributes: {name: "Hercules"}}}
family_api.Child.create(attributes={'name': "Hercules"})
# POST: {data: {type: "children", attributes: {name: "Hercules"}}}
family_api.Child.create(id="1", attributes={'name': "Hercules"})
# POST: {data: {type: "children", id: "1", attributes: {name: "Hercules"}}}
# ^^^^
Deleting
Deleting happens simply by calling .delete()
on an object. After deletion,
the object will have the same data as before, except its id
will be set to
None
. This happens in case you want to delete an object and instantly
re-create it, with a different ID.
child = family_api.Child.get("1")
child.delete()
# Will create a new child with the same name and parent as the previous one
child.save('name', 'parent')
child.id in (None, "1")
# False
Editing relationships
Singular relationships
Changing a singular relationship can happen in two ways (this also depends on
what the server supports).
child = family_api.Child.get("1")
child.parent = new_parent
child.save('parent')
# or
child.change('parent', new_parent)
The first one will send a PATCH request to /children/1
with a body of:
{"data": {"type": "children",
"id": "1",
"relationships": {"parent": {"data": {"type": "parents", "id": "2"}}}}}
The second one will send a PATCH request to the URL indicated by
child.relationships['parent']['links']['self']
, which will most likely be
something like /children/1/relationships/parent
, with a body of:
{"data": {"type": "parents", "id": "2"}}
If you want to use the first way, you could also change the relationship
directly:
child.relationships['parent'] = {'data': {'type': "parents", 'id': "2"}}
child.save('parent')
However, this poses a danger. relationships
and related
are supposed to be
in sync with each other and, if you change one or the other directly, they may
stop being in sync which may generate some confusion later. A successful
.save()
will rewrite the relationships so you should be OK. However, if you
want to be safe, you should use the .set_related()
method to edit
relationships:
child.set_related('parent', family_api.Parent(id="2"))
or use the relationship's name shortcut:
child.parent = family_api.Parent(id="2")
(the shortcut uses .set_related()
during assignment internally anyway)
Plural relationships
For changing plural relationships, you can use one of the add
, remove
and
reset
methods:
parent = family_api.Parent.get("1")
parent.add('children', [new_child, ...])
parent.remove('children', [existing_child, ...])
parent.reset('children', [child_a, child_b, ...])
These will send a POST, DELETE or PATCH request respectively to the URL
indicated by parent.relationships['children']['links']['self']
, which will
most likely be something like /parents/1/relationships/children
, with a body
of:
{"data": [{"type": "children", "id": "1"},
{"type": "children", "id": "2"},
{"...": "..."}]}
Similar to the case when we were instantiating objects with relationships, the
values passed to the above methods can either be resource objects, "resource
identifiers" or entire relationship objects:
parent.add('children', [family_api.Child.get("1"),
family_api.Child(id="2"),
{'type': "children", 'id': "3"},
{'data': {'type': "children", 'id': "4"}}])
This way, you can easily use another object's plural relationship:
parent_a = family_api.Parent.get('1')
parent_b = family_api.Parent.get('2')
# Make sure 'parent_b' has the same children as 'parent_a'
parent_b.reset('children', list(parent_a.fetch('children').all()))
Bulk operations
Resource subclasses provide the bulk_delete
, bulk_create
and bulk_update
classmethods for API endpoints that support such operations. The arguments to
these class methods are quite flexible. Consult the docstrings of each method
for their types or see the following examples.
Furthermore, bulk_update
accepts a fields
keyword argument with the
attributes
and relationships
of the objects it will attempt to update.
# Bulk-create
family_api.Child.bulk_create([
family_api.Child(attributes={'name': "One"}, relationships={'parent': parent}),
{'attributes': {'name': "Two"}, 'relationships': {'parent': parent}},
({'name': "Three"}, {'parent': parent}),
])
# Bulk-update
child_a = family_api.Child.get("a")
child_a.married = True
family_api.Child.bulk_update(
[child_a,
{'id': "b", 'attributes': {'married': True}},
("c", {'married': True}), "d"],
fields=['married'],
)
# Bulk delete
child_a = family_api.Child.get("a")
family_api.Child.bulk_delete([child_a, {'id': "b"}, "c"])
parent = family_api.Parent.get("1")
family_api.Child.delete(list(parent.children.all()))
For more details, see our
bulk oprations {json:api} profile.
Form uploads, redirects
If an endpoint accepts other content-types apart from
application/vnd.api+json
during creation (most likely a multipart/form-data
for file uploads), you can perform such requests using the .create_with_form
classmethod. The keyword arguments you provide will be passed to the requests
library, giving you complete control over the request you want to perform.
According to {json:api}'s recommendations, an endpoint may return a
303-redirect response. If that's the case for a .get()
or .reload()
call,
the object's id
, attributes
, links
, relationships
and related
attributes will be empty. What will be there is a redirect
attribute set to
the response's Location
header's value. Calling .follow()
on such an object
will retrieve that location and process the response using the appropriate
class.
Given these two mechanisms, here is how you might go about performing a
source file upload
in Transifex API:
@TransifexApi.register
class TxResource(Resource)
TYPE = "resources"
@TransifexApi.register
class ResourceStringsAsyncUpload(Resource)
TYPE = "resource_strings_async_uploads"
@TransifexApi.register
class ResourceString(Resource)
TYPE = "resource_strings"
transifex_api = TransifexApi(...)
resource = transifex_api.TxResource.get(...)
with open(...) as f:
upload = transifex_api.ResourceStringsAsyncUpload.create_with_form(
data={'resource': resource.id},
files={'content': f},
)
while True:
if upload.redirect:
strings = upload.follow()
break
sleep(5)
upload.reload()