Skip to content

Commit 78bdc99

Browse files
authored
Merge pull request #76 from anomaly/alpha-10
Alpha 10
2 parents 99978c8 + 6d9ff57 commit 78bdc99

File tree

8 files changed

+710
-624
lines changed

8 files changed

+710
-624
lines changed

docs/docs/installation.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ following this you can call any of the SDK methods and the client will performan
4141
- `NoAPIKeyProvidedError` - If the API key is not set.
4242
- `ValueError` - If the API key does not conform to the expected format (which looks like eight tokens separated by `-`).
4343

44-
#### Using TLS Certificates
44+
#### Using TLS certificates
4545

4646
Command Centre optionally allows you to use self signed client side TLS certificates for authentication. You can use this along side your API key as an additional layer of security.
4747

@@ -74,7 +74,7 @@ The rest of the requests and operations remain the same, the library will use an
7474

7575
> Our testsuites are configured to run with and without TLS certificates to ensure that we support both modes of operation.
7676
77-
In instances (such as Github actions, where we store the certificate and key in the Github secrets manager) where you can't store the certficiate and key in the filesystem, you can use Python's `tempfile` module to create temporary files and clean up once you are done using them.
77+
In instances (such as Github actions, where we store the certificate and key in the Github secrets manager) where you can't store the certificate and key in the filesystem, you can use Python's `tempfile` module to create temporary files and clean up once you are done using them.
7878

7979
```python
8080
import tempfile
@@ -112,13 +112,13 @@ cc.file_tls_certificate = temp_file_certificate.name
112112
cc.file_private_key = temp_file_private_key.name
113113
```
114114

115-
### Command Line Interface
115+
### Command line interface
116116

117-
### Terminal User Interface
117+
### Terminal user interface
118118

119-
### SQL Support
119+
### SQL support
120120

121-
## Developer Notes
121+
## Developer notes
122122

123123
This library uses [httpx](https://www.python-httpx.org) as the HTTP transport and [pydantic](https://pydantic.dev) to construct and ingest payloads. We use [taskfile](https://taskfile.dev) to run tasks. Our test suite is setup using `pytest`.
124124

@@ -140,7 +140,7 @@ Some of the `task` targets take parameters e.g.
140140

141141
`task test` will run the entire test suite, while `task test -- test_cardholder.py` will run only the tests in `test_cardholder.py`.
142142

143-
### Building the Docs
143+
### Building the docs
144144

145145
The documentation is build using [mkdocs](https://www.mkdocs.org) and hosted on [Github pages](https://anomaly.github.io/gallagher/). The project repository is configured to build and publish the documentation on every commit to the `master` branch.
146146

docs/docs/python-sdk.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ poetry add gallagher
2626

2727
For production application please make sure you target a particular version of the API client to avoid breaking changes.
2828

29-
## Data Transfer Objects (DTO) Premiere
29+
## Data Transfer Objects (DTO) premiere
3030

3131
The Data Transfer Objects or DTOs are the centre piece of the Python SDK. These are built using the much loved [pyndatic](https://pydantic.dev) library. The aim is strict validation of responses and request payloads to ensure that the SDK never falls out of line with Gallagher' REST API.
3232

@@ -47,7 +47,7 @@ In addition to DTOs, you will see a number of :
4747

4848
If you are fetching a `detail` then they are returned on their own as part of the response. They typically contain `href` to related objects.
4949

50-
## API Endpoint Lifecycle
50+
## API endpoint lifecycle
5151

5252
You do not need to look under the hood to work with the API client. This section was written for you to understand how we implement Gallagher's requirements for standard based development. Each endpoint inherits from a base class called `APIEndpoint` defined in `gallagher/cc/core.py` and provides a configuration that describes the behaviour of the endpoint (in accordance with the Command Centre API).
5353

@@ -115,7 +115,7 @@ cc.api_base = URL.CLOUD_GATEWAY_US
115115

116116
In cases where you are targeting a local Command Centre, you can set the `api_base` to the FQDN or IP address of the Command Centre that's locally accessible on the network.
117117

118-
### Proxy Support
118+
### Proxy support
119119

120120
Thanks to `httpx` we have proxy support built in out of the box. By default the `proxy` is set to `None` indicating that one isn't in use. If you wish to use a proxy for your use case, then simply set the `proxy` attribute on the `cc` object like you would the `api_base` or `api_key`.
121121

@@ -270,7 +270,7 @@ while items_summary.next:
270270
determined from the response object. This ensures that we can update the SDK as the API changes
271271
leaving your code intact.
272272

273-
# Updates and Changes
273+
## Follow for changes
274274

275275
Entities like `Cardholders`, `Alarms`, `Items`, and `Event` provide `updates` or `changes`, that can be monitored for updates. Essentially these are long poll endpoints that:
276276

@@ -340,6 +340,10 @@ async def get_config(cls) -> EndpointConfig:
340340
)
341341
```
342342

343+
!!! warning
344+
345+
As a breaking change in `8.90` the operator must have the 'Create Events and Alarms' privilege in the division of the source item, if your request specifies a source item. Current versions only require that the operator has that privilege on at least one division.
346+
343347
## Error Handling
344348

345349
### Exceptions
@@ -363,6 +367,7 @@ Personal Data Definitions are fields associated to a cardholder and are defined
363367
- children of the `personalDataFields` key in the cardholder detail
364368
- accessible via key name prefixed with the `@` symbol i.e the personal data field `Email` is accessible via the key `@Email`
365369

370+
366371
!!! tip
367372

368373
Note that the `personDataFields` has a `list` of objects, and each object has a single key which is the nae of the personal data field and the value is the related data.
@@ -395,10 +400,33 @@ and we had used the API client to fetch the cardholder detail (partial example):
395400
cardholder = await Cardholder.retrieve(340)
396401
```
397402

398-
you could access the `Email` field either via iterating over `cardholder.personal_data_definitions` and looking to match the `key` attribute of the object to `@Email` or using the parsed shortcut `cardholder.pdf.email`.
403+
`cardholder` would have two fields:
404+
- `personal_data_definitions` which is a list of `CardholderPersonalDataDefinition` objects
405+
- `pdf` which is a parsed object of the personal data fields
399406

400-
The above is achieved by dynamically populating a placeholder object with dynamically generated keys. These are parsed and populate _once_ when the object has successfully parsed the `JSON` payload.
407+
`cardholder.personal_data_definitions` is iterable, each instance exposing a `name` and `contents` fields. Use the `value` attribute of `contents` to access the PDF value:
408+
409+
```python
410+
for pdf in cardholder.personal_data_definitions:
411+
if pdf.name == '@Email':
412+
print(pdf.name, pdf.contents.value)
413+
```
401414

402415
!!! tip
403416

404417
See pyndatic's [Model validator](https://docs.pydantic.dev/latest/concepts/validators/#model-validators) feature in v2, in particular the `@model_validator(mode='after')` constructor.
418+
419+
The `cardholder` object will also expose a special attribute called `pdf`. Each instance available in the `personal_data_definitions` field will be mapped to a Pythonic `snake_cased` key, that lets you access the same `CardholderPersonalDataDefinition` object via the `@` prefixed key name. So the above example of accessing the `@Email` field can be done as follows:
420+
421+
```python
422+
cardholder.pdf.email.value
423+
```
424+
425+
The `pdf` attribute is dynamically populated object with dynamically generated keys. Here are some examples of how `PDF` field names are mapped to `snake_case` keys:
426+
427+
- `@Cardholder UID` would become `pdf.cardholder_uid`
428+
- `@City` would become `pdf.city`
429+
- `@Company Name` would become `pdf.company_name`
430+
- `@PINNumber` would become `pdf.pin_number`
431+
432+
Both approaches have their merits and you should use the one that suits your use case.

gallagher/cc/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ async def follow(
526526
f"{url}", # required to turn pydantic object to str
527527
headers=_get_authorization_headers(),
528528
params=params,
529-
timeout=TRANSPORT.TIMEOUT_POLL, # Next Gallagher CC wait
529+
timeout=TRANSPORT.TIMEOUT_POLL,
530530
)
531531

532532
if response.status_code == HTTPStatus.OK:

gallagher/dto/detail/cardholder.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
from typing import Optional, Any
33
from typing_extensions import Self
44

5-
from pydantic import model_validator, Field
5+
from pydantic import model_validator
66

77
from ..utils import (
88
AppBaseModel,
99
IdentityMixin,
1010
HrefMixin,
11+
OptionalHrefMixin,
12+
_to_snake_case_key,
1113
)
1214

1315
from ..ref import (
@@ -111,8 +113,8 @@ class CardholderDetail(
111113
# operator_groups
112114
# competencies
113115

114-
edit: HrefMixin
115-
update_location: HrefMixin
116+
edit: OptionalHrefMixin = None
117+
update_location: OptionalHrefMixin = None
116118
notes: Optional[str] = None
117119

118120
# notifications
@@ -164,9 +166,9 @@ def populate_pdf_accessor(self) -> Self:
164166
# if you compare these in the tests, they should be the same
165167
setattr(
166168
self.pdf,
167-
# replace spaces with underscores and make it lowercase
169+
# turn the key into a snake case key
168170
# ignore the prefixed @ symbol
169-
pdf_field.name[1:].replace(' ', '_').lower(),
171+
_to_snake_case_key(pdf_field.name),
170172
pdf_field.contents
171173
)
172174

gallagher/dto/summary/pdf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
from ..utils import (
66
AppBaseModel,
7-
HrefMixin,
7+
OptionalHrefMixin,
88
IdentityMixin,
99
)
1010

1111
class PdfSummary(
1212
AppBaseModel,
13-
HrefMixin,
13+
OptionalHrefMixin,
1414
IdentityMixin,
1515
):
1616
""" Personal Data Field Summary

gallagher/dto/utils.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from typing import (
1919
Optional,
2020
)
21-
from functools import cached_property
21+
22+
import re
23+
2224
from typing_extensions import Annotated
2325

2426
from datetime import datetime
@@ -52,10 +54,36 @@ def _to_lower_camel(name: str) -> str:
5254
return upper[:1].lower() + upper[1:]
5355

5456

57+
def _to_snake_case_key(key: str) -> str:
58+
""" Changes keys to snake_case
59+
60+
This is used to convert keys from the Gallagher API to
61+
snake_case. Primarily used to change PDF field names to
62+
snake_case fields that are dynamically created.
63+
64+
1. Strip leading '@'
65+
2. Replace spaces or dashes with underscores
66+
3. Split acronym/word boundary: e.g. PINNumber → PIN_Number
67+
4. Split camelCase boundary: e.g. fooBar → foo_Bar
68+
5. Collapse multiple underscores
69+
6. Lowercase
70+
"""
71+
# 1) remove leading '@'
72+
key = key.lstrip('@')
73+
# 2) normalize spaces/dashes to underscore
74+
key = re.sub(r'[\s\-]+', '_', key)
75+
# 3) acronym boundary: uppercase-seq → uppercase + lowercase
76+
key = re.sub(r'(?<=[A-Z])(?=[A-Z][a-z])', '_', key)
77+
# 4) camelCase boundary: lowercase/digit → uppercase
78+
key = re.sub(r'(?<=[a-z0-9])(?=[A-Z])', '_', key)
79+
# 5) collapse multiple underscores
80+
key = re.sub(r'__+', '_', key)
81+
# 6) lowercase
82+
return key.lower()
83+
5584
# Ensure that the primitive wrappers such as Mixins appear
5685
# before the generic classes for parsing utilities
5786

58-
5987
class IdentityMixin(BaseModel):
6088
"""Identifier
6189

0 commit comments

Comments
 (0)