# The OpenApi3 Client ## Accessing ESI Django-ESI aims to provide a convenience wrapper around [AIOpenAPI3](https://github.com/commonism/aiopenapi3) - A Bravado Style Wrapper providing convenient Endpoint.results() functions and more - Access to the Raw "Sad Smiley" Interface from AIOpenAPI3 - Python Stubs for easier development ### Getting a client object All access to ESI happens through a dynamically generated Client. {ref}`ESIClientProvider ` It is strongly recommended to re-use this Client wherever possible. Generating a new client is slow and in concurrent environments could lead to memory leaks. ### Important notes on memory use The new client provider can use substantially more memory than the old swagger client. This is due to the in memory pydantic models that are being created. There are now parameters for your client to only load the methods or tags that it needs, testing has shown this to be in the realm of up to 90mb's of wasted RAM use per client without filtering enabled. This will add up very fast with multiple apps running different clients. Failing to use a Tag or an Operation will throw an AttributeError("No tag/path filtering supplied to ESI Client."). If settings.DEBUG=True this exception is not thrown and a warning is printed to the logger, this is useful for tests, development and stub generation. ### Example for creating a Provider Your provider should be instantiated at import time, usually in a `providers.py` then imported to the other modules that need a client, or results. Below are examples of creating some clients with both operation and tag filters and some helper functions to use the endpoints, ultimately how you do this is up to you. #### Client with a specific endpoints This example creates a client with only [GetAlliances](https://developers.eveonline.com/api-explorer#/operations/GetAlliances) and [GetAlliancesAllianceId](https://developers.eveonline.com/api-explorer#/operations/GetAlliancesAllianceId) operations. ```python # providers.py from esi.openapi_clients import ESIClientProvider # Generate a client esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", operations=["GetAlliances", "GetAlliancesAllianceId"] ) def get_alliances(): operation = esi.client.Alliance.GetAlliances() alliances = operation.results() return alliances def get_alliances_alliance_id(alliance_id): operation = esi.client.Alliance.GetAlliancesAllianceId(alliance_id=alliance_id) detail = operation.result() return detail ``` ```python # tasks.py / views.py from .providers import get_alliances ... ``` #### Client with a single Tag This example creates a client with all operations under the [Alliance Tag](https://developers.eveonline.com/api-explorer#/operations/GetAlliances) ```python # providers.py from esi.openapi_clients import ESIClientProvider # Generate a client esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", tags=["Alliance"] ) def get_alliances(): operation = esi.client.Alliance.GetAlliances() alliances = operation.results() return alliances def get_alliances_alliance_id(alliance_id): operation = esi.client.Alliance.GetAlliancesAllianceId(alliance_id=alliance_id) detail = operation.result() return detail def get_list_alliance_corporations(alliance_id): operation = esi.client.Alliance.GetAlliancesAllianceIdCorporations(alliance_id=alliance_id) corporations = operation.result() return corporations def get_alliance_icon_urls(alliance_id): operation = esi.client.Alliance.GetAlliancesAllianceIdIcons(alliance_id=alliance_id) urls = operation.result() return urls ``` ```python # tasks.py / views.py from .providers import get_alliances, get_alliances_alliance_id, get_list_alliance_corporations, get_alliance_icon_urls ... ``` ### Using public endpoints Here is a complete example how to use a public endpoint. Public endpoints can in general be accessed without any authentication. ```python from esi.openapi_clients import ESIClientProvider # Generate a client esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", operations=["GetAlliances"] ) def main(): # Create a ESI Request operation = esi.client.Alliance.GetAlliances() # Send the Request, with return_response=True to receive alliances = operation.results() # Do Things! print(alliances) main() ``` ### Using authenticated endpoints Non-public endpoints will require authentication. You will therefore need to provide a valid access token with your request. The following example shows how to retrieve data from a non-public endpoint using an already existing token in your database. :::{seealso} See also the section {ref}`section-usage-in-views` on how to create tokens in your app. ::: ```python from esi.helpers import get_token from esi.openapi_clients import ESIClientProvider # Generate a client esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", operations=["GetCharactersCharacterIdAssets"] ) def fetch_assets(): req_scopes = ['esi-assets.read_assets.v1'] token = get_token(90406623, req_scopes) res = esi.client.Assets.GetCharactersCharacterIdAssets( character_id=90406623, token=token, ) assets = res.results() return(assets) fetch_assets() ``` ### results() vs. result() Django-ESI offers two similar methods for requesting the response from an endpoint: results() and result(). Here is a quick overview how they differ: | | Paging | Headers | | ------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------ | | **results()** | Automatically returns all data through pages or cursors if there is more than one | Returns the headers for the last retrieved page | | **result()** | Only returns the first page or the requested page (when specified with page parameter) | Returns the headers for the first requested page | | **cursor()** | None, WIP | | In general we recommend to use results(), so you don't have to worry about paging. Nevertheless, result() gives you more direct control of your API request and has it's uses, e.g when you are only interested in the first page and do not want to wait for all pages to download from the API. ### Getting localized responses from ESI Some ESI endpoints support localization, which means they are able to return the content localized in one of the supported languages. To retrieve localized content just provide the language code in your request. The following example will retrieve the type info for the Svipul in Korean: ```python result = esi.client.Universe.GetUniverseTypesTypeId( type_id=11567, Accept_Language='ko' ).results() ``` A common use case it to retrieve localizations for all languages for the current request. For this django-esi provides the convenience method `results_localized()`. It substitutes `results()` and will return the response in all officially supported languages by default. ```python results_localized = esi.client.Universe.GetUniverseTypesTypeId( type_id=11567 ).results_localized() ``` Alternatively you can pass the list of languages (as language code) that you are interested in: ```python results_localized = esi.client.Universe.GetUniverseTypesTypeId( type_id=11567 ).results_localized(languages=['ko', 'de']) ``` ## Rate Limiting and Exceptions ESI currently has a few separate rate limiting mechanisms. ### [The Global Error Limit](https://developers.eveonline.com/docs/services/esi/best-practices/#error-limit) If you trip the global error limit (currently 100errors/60seconds) ESIClientProvider will raise a `ESIErrorLimitException()`, Your apps should either: - retry their celery tasks after the reset timer, Preferred, non blocking - wrap requests in the `wait_for_esi_error_limit_reset()` decorator, which will block other celery tasks from running. ### Custom Rate Limits Two are currently known - Market Data History, 300 Requests / 60 seconds - Character Corporation History, 300 Requests / 60 Seconds Wrapping your requests in the `esi_rate_limiter_bucketed()` decorator will raise `ESIBucketLimitException(bucket)` if you exceed the rate limit. - retry their celery tasks after the reset timer, Preferred, non blocking - wrap requests in the `esi_rate_limiter_bucketed(raise_on_limit=False)` decorator, which will block other celery tasks from running. ### [Floating Window Rate Limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/) These rate limits are exposed in the specifications e.g. [GetStatus](https://developers.eveonline.com/api-explorer#/operations/GetStatus) `This route is part of the rate limit group status. This group is limited to 600 tokens per 15 minutes.` Our client dynamically extracts these rate limits from the spec per endpoint/grouping and tracks their refresh from headers. Exhausting these Rate Limits will raise `ESIBucketLimitException(bucket)` #### Token System (Copied from for awareness) As shown, you are encouraged to use ETags (If-Match) | Status Code | Token Cost | Reasoning | | ----------- | ---------- | ------------------------------------------------------ | | 2XX | 2 tokens | | | 3XX | 1 token | Promote the use of `If-Modified-Since` and `If-Match`. | | 4XX | 5 tokens | Discourage hitting user-errors. | | 5XX | 0 tokens | You shouldn't be penalized for server-side errors. | ## Compatibility Date Header After [2025-07-10 This Blog Post](https://developers.eveonline.com/blog/changing-versions-v42-was-getting-out-of-hand) ESI will now be versioned as a whole spec, and not with versioned paths specific to each endpoint. Compatibility Date must be included in order to generate an ESIClientProvider instance, in the format YYYY-MM-DD. ```{note} You can also provide a python date object (`datetime.date`) to the ESIClientProvider. As a compatibility date to the client factory. ``` This header tells ESI, "This application's ESI implementation was updated or reviewed at this date - give me the API behavior as it was at that date", please use a date that you genuinely tested or built your app. Setting this to "today" will cause you issues later. For best results, you should develop against Django-ESI with a consistent specific build date in mind which will Type Hint your code, Set this as your Compatibility Date and only roll forward once fully tested. Django-ESI can still be updated safely by users as your Compatibility Date will still define the ESIs behaviour. ## User Agent header CCP asks developers to provide a "good User-Agent header" with all requests to ESI, so that CCP can identify which app the request belongs to and is able to contact the server owner running the app in case of any issues. This requirement is specified in the CCP's [Developer Guidelines](https://developers.eveonline.com/resource/resources) and detailed in the [ESI guidelines](https://docs.esi.evetech.net/docs/guidelines.html). Django-ESI provides a user-agent generator for setting the User-Agent header following a consistent format: ### Application Info When using {ref}`ESIClientProvider ` Two parameters `ua_appname` and `ua_version` are required to build the user-agent dynamically, whilst `ua_url` may be optionally included to link to a Repository or Documentation. Our User-Agent Generator follows [RFC9110](https://www.rfc-editor.org/rfc/rfc9110#name-user-agent) and [The MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) #### Application Name Format The `ua_appname` should be formatted as PascalCase without spaces and/or hyphens, and `ua_version` should follow [Semantic Versioning](https://semver.org/). If your `ua_appname` contains spaces or hyphens, they will be removed and the following character will be capitalized to convert it to PascalCase. The conversion follows this defined behaviour: Any string containing spaces or hyphens will be converted to PascalCase. Strings without spaces or hyphens will be returned unchanged. This gives you the opportunity to use already formatted strings as needed. #### Examples | Input Format | Output Format | | ------------ | ------------- | | app name | AppName | | app-name | AppName | | appname | appname | | AppName | AppName | | appName | appName | | app_name | app_name | ```text AppName/1.2.3 (foo@example.com) DjangoEsi/1.2.3 (+https://gitlab.com/allianceauth/django-esi) ``` or if ua_url is specified ```text AppName/1.2.3 (foo@example.com; +https://gitlab.com/) DjangoEsi/1.2.3 (+https://gitlab.com/allianceauth/django-esi) ``` Here is a complete example for defining an application string with your app: ```python from esi.openapi_clients import ESIClientProvider esi = ESIClientProvider(ua_appname="AppName", ua_version="1.2.3", ua_url="https://gitlab.com/") ``` ::::{hint} Linking to this information Dynamically will reduce duplication ```python from esi.openapi_clients import ESIClientProvider from . import __title__, __version__, __url__ esi = ESIClientProvider(ua_appname=__title__, ua_version=__version__, ua_url=__url__) ``` :::: :::{note} If you do not define both `ua_appname` and `ua_version`, the application string used will be `"DjangoEsi/1.2.3 (+https://gitlab.com/allianceauth/django-esi)"`. ::: ### Contact email To enable CCP to contact the maintainer of a server that is using ESI it is mandatory to specify a contact email. This can be done through the setting ESI_USER_CONTACT_EMAIL. Example: ```python ESI_USER_CONTACT_EMAIL = "admin@example.com" ``` In case you are not hosting the app yourself, we would recommend including this setting in the installation guide for your app. ## Caching / ETags, Why and How Django-ESI 8.x and the OpenAPI client now assume your data is, and should be cached. This is part of an ongoing effort to be more respectful of ESI as a shared resource and our impact on it. You should stay with the default Caching and ETags where possible, but also consider the frequency with with you pull data and the Redis/Memory demands of the hosts of your applications. ### Cache: Default = True Caching requests will store the entire HTTP response in memory. If you have a large set of data you pull much less regularly than the expected cache TTL, or you _know_ you will never want the cached response consider not saving it to save memory. You may also, in some specific circumstances wish to forcibly ignore our cache and pull data from ESI for diagnostics or maintenance reasons. ```docstring use_cache -- check cache prior to fetching from ESI (default True) store_cache -- store the returned data from ESI in cache (default True) ``` - PRO: Allows you full access to the HTTP Response to work on in-place. - PRO: Does not hit ESI at all whilst the cache is valid. - CON: Consumes the most memory - CON: Shortlived, Cached for Max-Age or Expires which can vary on the endpoint - CON: Not Smart, if CCP invalidate the cache voluntarily our cache will hold on to the data until it expires. ### ETags (If-None-Match): Default = True An ETag is a hash of the content of a response. These allow you to ask ESI politely if the response has _changed_. If the response has not changed, you will receive HTTPNotModified. Again, you can not send or store ETags for specific reasons with the following. ```docstring use_etag -- Use the inbuilt ETag matching system (default True) ``` - PRO: Minimal Memory Usage - CON: Does hit ESI (consuming 1 Token where relevant) - CON: Does not return the HTTP Response if you need it. - NOTE: Decent life, currently 7 Days. ### Last-Modified (If-Modified-Since): Optional Datetime Similar to an ETag, this is the datetime the resource was last modified. These allow you to query if an object has changed since the last time you pulled the data, irrelevant of the content. If this matches you will receive HTTPNotModified Unlike ETags, If-Modified-Since is not managed internally in Django-ESI (Yet?), instead you can store them along side your models and pass it with your request, These are very situational although improvements on CCPs end may end up meaning these see a lot more use in time to come. ```docstring last_modified -- Optional datetime to send as If-Modified-Since ``` - PRO: No Memory Usage - PRO: Valid forever? if an object doesn't change for a year, neither will If-Modified-Since - Does Hit ESI (Consuming 1 Token where relevant) - CON: Does not return the HTTP Response if you need it. - CON: Requires additional handling. On single object models this is easy. ```python class Example(models.Model): example_id = models.PositiveIntegerField(unique=True) last_updated = models.DateTimeField(default=now) # auto_now_add=True doesnt allow override from Last_Modified header ``` ### Force Refresh **Use with responsibility**. This will not only not use ETags and the HTTP Cache, **it will wipe them**. ```docstring force_refresh -- clear ETag and cache, force a re-fetch from ESI (default False) ``` - PRO: Sometimes a hammer is the right tool for a job - CON: Sometimes a hammer is not the right tool for a job... - CON: Will affect following tasks impact on ESI ## Advanced Features ### Using a local spec file Using a local resource file is less useful in OpenAPI3 than it was with Swagger. You may pass the `spec__file` parameter to your local `openapi.json`, but you will be missing out on the [Non Breaking Features](https://developers.eveonline.com/docs/services/esi/overview/#breaking-changes) that ccp may add to a given compatibility date. Of course this may be a reason to use a `spec_file`, but it it is the hope with OpenAPI3 these are indeed _non breaking changes_ time will tell. ```python import os from esi.openapi_clients import ESIClientProvider SWAGGER_SPEC = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'openapi.json') esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", spec_file=SWAGGER_SPEC ) ``` ### Getting Response Data Sometimes you may want to also get the internal response object from an ESI response. For example to inspect the response header. For that simply use `return_response=True` in your call. This works in the same way for both `.result()` and `.results()` ```python from esi.openapi_clients import ESIClientProvider # Generate a client esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", operations=["GetAlliances"] ) def main(): # Create a ESI Request operation = esi.client.Alliance.GetAlliances() # Send the Request, with return_response=True to receive alliances, response = operation.results(return_response=True) # Do Things! print(alliances, response) ``` ::::{only} never ### Accessing alternate data sources Currently Functionally Useless, CCP use this internally but there is only one Tenant exposed publicly. If `singularity` or the AT servers are ever exposed again, this may be of some use. :::{hint} This used to be Datasource in Swagger ::: ```python from esi.openapi_clients # create your own provider esi = ESIClientProvider(tenant='tranquility') ``` :::: ### Using the Raw AIOpenAPI3 Interface The developer of AIOpenAPI3 calls this the "Sad Smiley" interface, named from `._.`, This will have none of our niceties or faux-bravado wrapper, but the client is available should you wish. ```python esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1", operations=["GetAlliances"] ) req = esi._.GetAlliances alliances = req() print(alliances) ``` ## Exploring ESI Endpoints Three options are available - CCP's [API Explorer](https://developers.eveonline.com/api-explorer) - Python Autocomplete, your IDE should expose Tags and Operations if you generate a client and type `esi.client.` - Manually from a Django Shell ```python from esi.openapi_clients import ESIClientProvider # Generate a client esi = ESIClientProvider( compatibility_date = "2025-07-23", ua_appname = "MyProject", ua_version = "0.0.1a1" ) client = esi.client # Print all tags and their endpoints print("Available ESI endpoints:\n") for tag in sorted(client._tags): tag_obj = getattr(client, tag) print(f"[{tag}]") for op in sorted(tag_obj._operations): print(f" - {op}") print() ``` ```shell [Alliance] - GetAlliances - GetAlliancesAllianceId ... [Assets] - GetCharactersCharacterIdAssets ... [Calendar] - GetCharactersCharacterIdCalendar ... ``` ## Generating ESI Stubs Django-ESI Ships with a series of Stubs to make development easier, should you want to release a new version of Django-ESI or develop against a specific compatibility date with handy typehints `python manage.py generate_esi_stubs --compatibility_date=2020-01-01` For easier release management, compatibility_date is set to `__build_date__` of Django-ESI by default.