Skip to content

Commit

Permalink
Merge pull request #7 from edanalytics/rc/1.0.0
Browse files Browse the repository at this point in the history
Rc/1.0.0
  • Loading branch information
jayckaiser authored Aug 18, 2023
2 parents 2bd4e07 + dc1ad6e commit 31647ec
Show file tree
Hide file tree
Showing 7 changed files with 411 additions and 120 deletions.
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# edfi_api_client v0.2.0
## New Features
- `EdFiClient.get_swagger()` now returns an EdFiSwagger class that parses OpenAPI Swagger specification.
- `EdFiClient.resources` and `EdFiClient.descriptors` lazily retrieves lists of respective endpoints from Swagger.
- `EdFiEndpoint` child class attributes `description` and `has_deletes` lazily retrieves this metadata from Swagger.

## Under the hood
- Requests re-authenticate automatically, based on the expiration-time retrieved from the API.


# edfi_api_client v0.1.4
## Fixes
- Compatibility fix for Ed-Fi 6.0: casing changed for change version API responses


# edfi_api_client v0.1.2
## New features
- New "reverse_paging" pagination method for `EdFiResource.get_pages()`
Expand All @@ -12,9 +23,11 @@
## Fixes
- Fix bug in `EdFiResource.get_pages()` where default `change_version_step_size` was used instead of argument


# edfi_api_client v0.1.1
## Fixes
- retry on 500 errors
- Retry on 500 errors


# edfi_api_client v0.1.0
Initial release
87 changes: 85 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,50 @@ Client key and secret not provided. Connection with ODS will not be attempted.
Connection to ODS successful!
```

### Attributes

Authentication with the ODS is not required:

<details>
<summary><code>resources</code></summary>

-----

### resources
This method is unavailable in Ed-Fi2.

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

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

-----

</details>


<details>
<summary><code>descriptors</code></summary>

-----

### descriptors
This method is unavailable in Ed-Fi2.

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

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

</details>



### Methods

Authentication with the ODS is not required:
Expand Down Expand Up @@ -187,8 +231,7 @@ If `component` is unspecified, `resources` will be collected.
...}
```

Note: the returned dictionary is large and unwieldy*.
A future update will add an `EdFiSwagger` class to assist in navigation.
Returns an `EdFiSwagger` class containing the complete JSON payload, as well as extracted metadata from the Swagger.

-----

Expand Down Expand Up @@ -348,6 +391,46 @@ All methods that return `EdFiEndpoint` and child classes require a session with
<Enrollment Composite [edFi/students]>
```

### Attributes

<details>
<summary><code>description</code></summary>

-----

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

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


-----

</details>


<details>
<summary><code>has_deletes</code></summary>

-----

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

```python
>>> api.resource('bellSchedules').has_deletes
True
```

-----

</details>



### Methods

<details>
Expand Down
82 changes: 75 additions & 7 deletions edfi_api_client/edfi_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from typing import Callable, Optional

from edfi_api_client import util
from edfi_api_client.edfi_endpoint import EdFiResource, EdFiComposite
from edfi_api_client.edfi_endpoint import EdFiResource, EdFiDescriptor, EdFiComposite
from edfi_api_client.edfi_swagger import EdFiSwagger


class EdFiClient:
Expand Down Expand Up @@ -69,6 +70,15 @@ def __init__(self,
self.version_url_string = self._get_version_url_string()
self.instance_locator = self.get_instance_locator()

# Swagger variables for populating resource metadata (retrieved lazily)
self.swaggers = {
'resources' : None,
'descriptors': None,
'composites' : None,
}
self._resources = None
self._descriptors = None

# If ID and secret are passed, build a session.
self.session = None

Expand Down Expand Up @@ -151,7 +161,8 @@ def get_data_model_version(self) -> Optional[str]:
return None


def get_swagger(self, component: str = 'resources') -> dict:
### Methods related to retrieving the Swagger or attributes retrieved therein
def get_swagger(self, component: str = 'resources') -> EdFiSwagger:
"""
OpenAPI Specification describes the entire Ed-Fi API surface in a
JSON payload.
Expand All @@ -163,7 +174,49 @@ def get_swagger(self, component: str = 'resources') -> dict:
swagger_url = util.url_join(
self.base_url, 'metadata', self.version_url_string, component, 'swagger.json'
)
return requests.get(swagger_url, verify=self.verify_ssl).json()

payload = requests.get(swagger_url, verify=self.verify_ssl).json()
swagger = EdFiSwagger(component, payload)

# Save the swagger in memory to save time on subsequent calls.
self.swaggers[component] = swagger
return swagger

def _set_swagger(self, component: str):
"""
Populate the respective swagger object in `self.swaggers` if not already populated.
:param component:
:return:
"""
if self.swaggers.get(component) is None:
self.verbose_log(
f"[Get {component.title()} Swagger] Retrieving Swagger into memory..."
)
self.get_swagger(component)


@property
def resources(self):
"""
:return:
"""
if self._resources is None:
self._set_swagger('resources')
self._resources = self.swaggers['resources'].endpoints
return self._resources

@property
def descriptors(self):
"""
:return:
"""
if self._descriptors is None:
self._set_swagger('descriptors')
self._descriptors = self.swaggers['descriptors'].endpoints
return self._descriptors


### Helper methods for building elements of endpoint URLs for GETs and POSTs
Expand Down Expand Up @@ -246,8 +299,9 @@ def connect(self) -> requests.Session:
self.session = requests.Session()
self.session.headers.update(req_header)

# Add new attribute to track when connection was established.
# Add new attributes to track when connection was established and when to refresh the access token.
self.session.timestamp_unix = int(time.time())
self.session.refresh_time = int(self.session.timestamp_unix + access_response.json().get('expires_in') - 120)
self.session.verify = self.verify_ssl

self.verbose_log("Connection to ODS successful!")
Expand Down Expand Up @@ -321,12 +375,13 @@ def descriptor(self,

params: Optional[dict] = None,
**kwargs
) -> EdFiResource:
) -> EdFiDescriptor:
"""
Even though descriptors and resources are accessed via the same endpoint,
this may not be known to users, so a separate method is defined.
"""
return self.resource(
return EdFiDescriptor(
client=self,
name=name, namespace=namespace, get_deletes=False,
params=params, **kwargs
)
Expand Down Expand Up @@ -384,6 +439,18 @@ def get_swagger(self, component: str = 'resources') -> dict:
"Swagger specification not implemented in Ed-Fi 2."
)

@property
def resources(self):
raise NotImplementedError(
"Resources collected from Swagger specification that is not implemented in Ed-Fi 2."
)

@property
def descriptors(self):
raise NotImplementedError(
"Descriptors collected from Swagger specification that is not implemented in Ed-Fi 2."
)


### Helper methods for building elements of endpoint URLs for GETs and POSTs
def _get_version_url_string(self) -> str:
Expand Down Expand Up @@ -442,8 +509,9 @@ def connect(self) -> requests.Session:
self.session.headers.update(req_header)
self.session.headers.update(json_header)

# Add new attribute to track when connection was established.
# Add new attributes to track when connection was established and when to refresh the access token.
self.session.timestamp_unix = int(time.time())
self.session.refresh_time = int(self.session.timestamp_unix + access_response.json().get('expires_in') - 120)
self.session.verify = self.verify_ssl

self.verbose_log("Connection to ODS successful!")
Expand Down
Loading

0 comments on commit 31647ec

Please sign in to comment.