Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - edanalytics/edfi_api_client · GitHub
Skip to content

Repository files navigation

Ed-Fi API Client Python Package

Quick Guide

fromedfi_api_clientimportEdFiClient# Client connection with Ed-Fi3 ODSapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3)
# Get the total row-count for the 'students' resource in the ODSstudents=api.resource('students')
students.get_total_count()
# Pull all rows for the 'staffs' resource deletes endpoint (setting a custom page-size)staffs=api.resource('staffs', get_deletes=True)
forrowinstaffs.get_rows(page_size=500):
pass# Pull all rows for the 'studentStaffAssociations' resource as pages (retrying when given authentication-timeout errors)ssa=api.resource('studentStaffAssociations') # OR 'student_staff_associations'forpageinssa.get_pages(retry_on_failure=True):
pass# Pull all rows for the enrollment students composite, filtering by section IDenrollment_students=api.composite('students', filter_type='sections', filter_id='12345')
forrowinenrollment_students.get_rows():
pass

EdFiClient

EdFiClient serves as the interface with the ODS. If credentials are provided, a session with the ODS is automatically authenticated. Some methods do not require credentials to be called.

Arguments:
ArgumentDescription
base_url[Required] The root url of the API server, without any trailing components like data/v3 or api/v2.0
client_keyThe key
client_secretThe secret
api_versionEither 2 or 3, depending on the suite number of the API (Default 3)
api_modeThe API mode of the ODS (e.g., shared_instance, year_specific, etc.). If empty, the mode will automatically be inferred from the ODS' Swagger spec (Ed-Fi 3 only).
api_yearThe year of data to connect to if accessing a year_specific or instance_year_specific ODS.
instance_codeThe instance code if accessing an instance_year_specific ODS.
use_snapshotBoolean flag for whether connected ODS is a snapshot (default False).
token_cacheAn optional token cache instance, such as edfi_api_client.token_cache.LockfileTokenCache, for storing OAuth bearer tokens to be shared among clients.

If either client_key or client_secret are empty, a session with the ODS will not be established.


All code examples in this document use verbose-logging to more-explicitly show interactions with the API. It is recommended to set verbose=True while working interactively with the API. If logging handlers have not been configured before enabling verbose-logging, default package handlers will be used.

>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, verbose=True)
Clientkeyandsecretnotprovided. ConnectionwithODSwillnotbeattempted.
# OR>>>fromedfi_api_clientimportEdFiClient>>>api=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, verbose=True)
ConnectiontoODSsuccessful!

Attributes

Authentication with the ODS is not required:

resources

resources

Retrieve a list of namespaced-resources from the resources Swagger payload.

>>>api.resources
[('ed-fi', 'academicWeeks'), ('ed-fi', 'accounts'), ('ed-fi', 'accountCodes'), ...]

descriptors

descriptors

Retrieve a list of namespaced-descriptors from the descriptors Swagger payload.

>>>api.descriptors
[('ed-fi', 'absenceEventCategoryDescriptors'), ('ed-fi', 'academicHonorCategoryDescriptors'), ...]

Methods

Authentication with the ODS is not required:

get_info

get_info

Ed-Fi3 provides an informative payload at the ODS base URL. This contains versioning by suite and build, API mode, and URLs for authentication and data management.

>>>api.get_info()
{'apiMode': 'Shared Instance',
'build': '2022.6.1.2034',
'dataModels': [{'name': 'Ed-Fi', 'version': '3.3.0-a'}],
'informationalVersion': '5.2',
'suite': '3',
'urls': {'dataManagementApi': '{BASE_URL}/data/v3/',
'dependencies': '{BASE_URL}/metadata/data/v3/dependencies',
'oauth': '{BASE_URL}/oauth/token',
'openApiMetadata': '{BASE_URL}/metadata/',
'xsdMetadata': '{BASE_URL}/metadata/xsd'},
'version': '5.2'}

get_api_mode

get_api_mode

Each Ed-Fi3 ODS has a declared API mode that alters how users interact with the ODS. This is a shortcut-method for finding the API mode of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info(), formatted in snake_case.

>>>api.get_api_mode()
'shared_instance'

This method is called automatically when api_mode is left undefined by the user.

get_ods_version

get_ods_version

This is a shortcut-method for finding the version of the Ed-Fi ODS via the payload retrieved using EdFiClient.get_info().

>>>api.get_ods_version()
'5.2'

get_data_model_version

get_data_model_version

This is a shortcut-method for finding the data model version of the Ed-Fi ODS' 'ed-fi' namespace via the payload retrieved using EdFiClient.get_info().

>>>api.get_data_model_version()
'3.3.0-a'

get_swagger

get_swagger

The entire Ed-Fi API is outlined in an OpenAPI Specification (i.e., Swagger Specification). There is a separate Swagger defined for each component type (e.g., resources, descriptors, etc.).

If component is unspecified, resources will be collected.

>>>api.get_swagger(component='resources') # Default
{'swagger': ...,
'basePath': ...,
'consumes': ...,
'definitions': ...,
...}

Returns an EdFiSwagger class containing the complete JSON payload, as well as extracted metadata from the Swagger.


is_edfi2

is_edfi2

Ed-Fi3 introduced many new features that are utilized heavily in this package.

>>>api.is_edfi2()
False

Package compatibility with Ed-Fi2 has been deprecated as of version 0.3.


Authentication with the ODS is required:

get_token_info

get_token_info

This method requires a connection to the ODS.

The Ed-Fi API provides a way to get information about the education organization related to a token. This method returns the oauth/token_info payload for the current session.

>>>api.get_token_info()
{'active': True, 'client_id': '', 'namespace_prefixes': [], 'education_organizations': [], 'assigned_profiles': []}

get_newest_change_version

get_newest_change_version

This method requires a connection to the ODS.

Starting in Ed-Fi3, each row in the ODS is linked to an ODS-wide "change version" parameter, which allows for narrow time-windows of data to be filtered for delta-ingestions, instead of only full-ingestions. This method returns the newest change version defined in the ODS.

>>>api.get_newest_change_version()
59084739

resource

resource

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and resource metadata from the API.

>>>api.resource(
name='students', # Name of resourcenamespace='ed-fi', # Default ; custom resources use a different namespaceget_deletes=False, # Default ; set to `True` to access the /deletes endpoint (mutually-exclusive with `get_key_changes`)get_key_changes=False, # Default ; set to `True` to access the /keyChanges endpoint (mutually-exclusive with `get_deletes`)params={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.


descriptor

descriptor

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiResource (i.e. EdFiEndpoint). This object contains methods to pull rows and descriptor metadata from the API.

Note that although descriptors and resources are saved at the same endpoint in the ODS, descriptors do not use their /deletes endpoint.

>>>api.descriptor(
name='sexDescriptors', # Name of descriptornamespace='ed-fi', # Default ; custom resources use a different namespaceparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<Resource [edFi/sexDescriptors]>

name, params, and kwargs can be formatted in snake_case or camelCase.


composite

composite

This method only requires a connection to the ODS when calling a REST method.

Use this method to initialize an EdFiComposite (i.e. EdFiEndpoint). This object contains methods to pull rows and composite metadata from the API.

Note: The only composite currently defined in the API is enrollment.

>>>api.composite(
name='students', # Name of composite resourcenamespace='ed-fi', # Default ; custom resources use a different namespacecomposite='enrollment', # Default ; name of compositefilter_type=None, # Optional; used to filter composites by ID and typefilter_id=None, # Optional; used to filter composites by ID and typeparams={}, # Optional; used to pass parameters to API calls**kwargs# Optional; alternative way to pass parameters to API calls
)
<EnrollmentComposite [edFi/students]>

name, params, and kwargs can be formatted in snake_case or camelCase.



EdFiEndpoint

EdFiEndpoint is an abstract base class for interfacing with API endpoints. All methods that return EdFiEndpoint and child classes require a session with the API.

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>students<Resourcewith2parameters [edFi/students]># AND/OR>>>students_composite=api.composite('students')
>>>students_composite<EnrollmentComposite [edFi/students]>

Attributes

description

description

This attribute retrieves the Ed-Fi endpoint's description if present in its respective Swagger payload.

>>>api.resource('bellSchedules').description'This entity represents the schedule of class period meeting times.'

has_deletes

has_deletes

This attribute returns whether a deletes path is present the Ed-Fi endpoint's respective Swagger payload.

>>>api.resource('bellSchedules').has_deletesTrue

Methods

ping

ping

This method pings the endpoint and returns a Response object with scrubbed JSON data. This offers a shortcut for verifying claim-set permissions without needing to pull data from the ODS.

>>>res=students.ping()
>>>res<Response [200]>>>>res.json()
{'message': 'Ping was successful! ODS data has been intentionally scrubbed from this response.'}

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get

get

This method retrieves one GET-request of JSON rows from the specified endpoint. This can be used to verify the structure of the data or to collect a small sample for testing.

An optional limit can be provided. If unspecified, the default limit will be retrieved. (This value must be less than the hard-coded limit of the ODS, or the request will fail.)

>>>students.get(limit=1)
[GetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[GetResource] Parameters: {}
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}]

Because this GET does not use pagination, the return is a list, not a generator.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_rows / get_pages

get_rows / get_pages

These are the primary methods for retrieving all JSON rows from the specified endpoint and parameters. The only difference in function is whether the rows are returned individually or in batches (i.e., pages). Iteration continues until no rows are returned.

Both methods use identical arguments. Under the hood, get_rows() implements get_pages(), but unnests the rows before returning.

>>>student_rows=students.get_rows(
page_size=500, # The limit to pass to the parameters. Overwrites parameter if already defined.retry_on_failure=False, # Reconnect session if request fails and reattempt (e.g., if authentication expires).max_retries=5, # If `retry_on_failure is True`, how many attempts before giving up.max_wait=500, # If `retry_on_failure is True`, max wait time for exponential backoff before giving up.step_change_version=False, # Only available for resources/descriptors. See [Change Version Stepping] below.change_version_step_size=50000, # Only available for resources/descriptors. See [Change Version Stepping] below.
)
<generatorobjectEdFiEndpoint.get_rowsat0x7f7472650f90>>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 4000}
[PagedGetResource] @ Retrieved135rows. Pagingoffset...
[PagedGetResource] @ Retrievedzerorows. Endingpagination.
[{'id': 'abc123', 'studentUniqueId': '987654', 'birthDate': '1970-01-01', ...}, ...]

To circumvent memory constraints, these methods return generators instead of lists.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.


get_total_count

get_total_count

This method returns the total count of rows for the given endpoint, as declared by the API. This action is completed by sending a limit 0 GET request to the API with the Total-Count header set to True.

>>>students.get_total_count()
4135

get_total_count() is currently only implemented for resources, not composites.

Note that params and retry-arguments can be passed to override those initialized in the EdFiEndpoint.



Change Version Stepping

The Ed-Fi API already has pagination built-in via the limit and offset parameters passed in GET-requests.

Here is an example of what calls to the API look like using pagination (page size 500), charted across time by change versions. EdFiPagination

This client provides a second type of pagination that uses change versions to improve performance when pulling from the API, referred to here as change version stepping.

A change version window of a specified length is defined, and calls to the API pass the min and max change versions of this window. Ordinary pagination still occurs within each window until zero rows are returned, after which the change version window steps and the process is repeated.

Here is an example of what calls to the API look like using change version stepping (step-window size 2000 and page size 500). EdFiChangeVersionStepping

Note that change versions are currently accessible only for resources, not for composites.

Why is change version stepping recommended when pulling from the API?

We can imagine requests sent to the Ed-Fi API as SQL select statements against the underlying ODS. For example, the code below makes repeated calls to the API, paging by 500 until all rows are retrieved.

>>>students=api.resource('students', schoolYear='2022')
>>>students.get_rows(page_size=500)

This code is semantically identical to the following SQL statements:

SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 0;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 500;
SELECT*FROM students WHERE schoolYear ='2022'LIMIT500 OFFSET 1000;
-- etc.

This works fine for small-volume resources. However, as offset increases, the computational-runtime of the query increases with it. For large-volume resources (e.g. studentSectionAttendanceEvents), this could translate to the following:

SELECT*FROM studentSectionAttendanceEvents LIMIT500 OFFSET 100000000;

This is the equivalent of calculating the first 100,000,500 rows of data, but only collecting the final 500. In practice, the connection to the ODS will time-out and need to re-authenticated before this query returns.

Luckily, the Ed-Fi3 "change versions" feature provides a helpful workaround for this. By specifying a min- and max-change-version in the query, a filtered select is applied that never reaches high offset.

>>>students=api.resource('students', min_change_version=0, max_change_version=50000)
>>>students.get_rows(page_size=500)

By definition, a change-version window will never contain more rows than the size of that window. Therefore, because the change version window defined above is only 50000 (i.e., max_change_version - min_change_version), the final API-call will be equivalent to the following:

SELECT*FROM students WHERE changeVersion BETWEEN 0AND50000LIMIT500 OFFSET 50000

Setting step_change_version = True in get_rows() or get_pages() turns on change version stepping. Use change_version_step_size to set the width of each stepping window (default 50000).

>>>students=api.resource('students', min_change_version=52028375, max_change_version=53295015)
>>>student_rows=students.get_rows(
page_size=500,
step_change_version=True,
change_version_step_size=50000# Default value. This is NOT optimized. Raise it to reduce API calls.
)
>>>list(student_rows)
[PagedGetResource] Endpoint : {BASE_URL}/data/v3/ed-fi/students
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved101rows. Pagingoffset...
[PagedGetResource] Parameters: {'minChangeVersion': 52028375, 'maxChangeVersion': 52078375, 'limit': 500, 'offset': 500}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] Parameters: {'minChangeVersion': 52078376, 'maxChangeVersion': 52128375, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrieved500rows. Pagingoffset...
# ...
[PagedGetResource] Parameters: {'minChangeVersion': 53278376, 'maxChangeVersion': 53295015, 'limit': 500, 'offset': 0}
[PagedGetResource] @ Retrievedzerorows. Steppingchangeversion...
[PagedGetResource] @ Changeversionexceededmax. Endingpagination.

To ingest all rows for a resource, find the ODS' newest change version and apply this to max_change_version, as below:

>>>max_change_version=api.get_newest_change_version()
>>>students=api.resource('students', min_change_version=0, max_change_version=max_change_version)
>>>students.get_rows(page_size=500, step_change_version=True)

Things to note when using change version stepping:

  • Change version stepping usually results in more requests made to the API; however, they are far less likely to overwhelm it as with high offsets.
  • Using change version stepping requires both min_change_version and max_change_version be defined within either the resource's params or kwargs. If either are undefined, an error is raised.
  • The default change_version_step_size is set to 50000. This value is not optimized. Try raising it to send fewer requests to the API.
  • API De-synchronization can occur when using Change-Version Stepping. See Reverse Paging.

Reverse Paging

There is a known problem that can occur when pulling from the API using change-version limits and without snapshotting. If any rows within the change-version window are updated mid-pull, their change-version is updated and they escape the window. When this occurs, all other rows in the window shift to fill the place of the missing row, resulting in rows entering previously pulled limit-offset pages and being missed in subsequent calls to the API. This leads to a gradual de-synchronization between the API and datalakes built from the API.

We have added a new offset-pagination method to counteract this bug, known as "reverse paging." By default, when step_change_version=True in resource pulls, requests are made to the API starting at the greatest offset and iterating backwards until offset zero. If a row is updated and a shift occurs mid-pull, one or more rows in the change version may be ingested multiple times, but no rows will be lost altogether.

For example:

Say there are 15 rows in the students resource with change versions between 0 and 20. We pull these rows using a page-size of 4.

EdFiDesync1

Say that before our fourth (and final) API call, record number 6 is updated and leaves the change-version window. Records 7 through 15 will shift to fill its place. When this occurs, record number 13 will shift from page 4 into a page that has already been ingested. Therefore, it will be missed from the final output.

EdFiDesync2

Using reverse-paging, page 4 will be ingested first. When record number 6 is updated and the rows shift, record 13 will move into page 3 and will be ingested a second time. However, this row will not be lost.


Token caching

Starting in version 7.3, EdFi Web API instances limit clients to 15 concurrent bearer tokens by default. In case an application uses multiple concurrent EdFiClients using the same client key and hitting the same API on a single machine or shared filesystem, this library provides a barebones on-disk cache to conserve tokens and avoid this limit.

fromedfi_api_clientimportEdFiClientfromedfi_api_client.token_cacheimportLockfileTokenCacheapi=EdFiClient(BASE_URL, CLIENT_KEY, CLIENT_SECRET, api_version=3, token_cache=LockfileTokenCache())
Arguments:
ArgumentDescription
token_cache_directoryPath to store tokens in. One cache is a JSON file containing an authentication payload, unique by OAuth URL and client key. (default ~/.edfi-tokens/)
write_lock_timeoutSeconds to wait to acquire a write lock if the cache is to be updated. (default 30)
write_lock_staleness_thresholdSeconds to wait after the last modified timestamp before forcibly deleting an existing lockfile (default 60). Aggressive by default, because a client won't try to obtain a write lock unless the payload inside is already expired or is corrupt.
write_lock_retry_delaySeconds to wait before retrying to acquire a write lock. (default 0.5)

Other kinds of shared caches may be implemented with the interface defined in edfi_api_client.token_cache.BaseTokenCache, should more sophisticated functionality be required (encryption, distributed caching, etc.).

About

No description, website, or topics provided.

Resources

Stars

15 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages