Skip to content

API Reference

Credential Management

To use the AdpApiClient you first must configure your API credentials. These credentials are managed through an AdpCredentials object, which can be configured manually or from environment variables.

Source code in src/adpapi/client.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@dataclass(frozen=True)
class AdpCredentials:
    client_id: str
    client_secret: str
    cert_path: str | None = CERT_DEFAULT
    key_path: str | None = KEY_DEFAULT

    @staticmethod
    def from_env() -> "AdpCredentials":
        client_id = os.getenv("CLIENT_ID")
        client_secret = os.getenv("CLIENT_SECRET")

        # Read optional mTLS certificate/key paths (defaults assume files in project root).
        cert_path = os.getenv("CERT_PATH")
        key_path = os.getenv("KEY_PATH")

        if cert_path is None:
            logger.warning(
                f"No environment variables found for CERT_PATH, defaulting to {CERT_DEFAULT}"
            )

        if key_path is None:
            logger.warning(
                f"No environment variables found for KEY_PATH, defaulting to {KEY_DEFAULT}"
            )

        if client_id is None or client_secret is None:
            raise ValueError("CLIENT_ID and CLIENT_SECRET environment variables must be set")

        return AdpCredentials(client_id, client_secret, cert_path, key_path)

Client

The main entry point for interacting with the ADP API. Using a context manager is recommended so the HTTP session is always closed cleanly:

from adpapi.client import AdpApiClient, AdpCredentials

credentials = AdpCredentials(
    client_id, client_secret, key_path, cert_path
)
with AdpApiClient(credentials) as api:
    api.call_endpoint(...)
    api.call_rest_endpoint(...)

The AdpApiClient surfaces two main entry points:

  • .call_endpoint() — for paginated OData queries (lists, searches)
  • .call_rest_endpoint() — for direct resource lookups by ID, with path parameter substitution

Call any Registered ADP Endpoint

Parameters:

Name Type Description Default
endpoint str

API Endpoint or qualified URL to call

required
select List[str]

Table Columns to pull

None
masked bool

Mask Sensitive Columns Containing Personally Identifiable Information. Defaults to True.

True
filters str | FilterExpression

OData Filter Expression. Strings will be passed directly, or OData strings can be automatically created from adpapi.odata_filters.FilterExpression objects

None
timeout int

Time to wait on. Defaults to 30.

DEFAULT_TIMEOUT
page_size int

Amount of records to pull per API call (max 100). Defaults to 100.

100
max_requests Optional[int]

Maximum number of requests to make (for quick testing). Defaults to None.

None

Raises:

Type Description
ValueError

When given an endpoint not following the call convention

Returns:

Type Description
list[dict]

List[Dict]: The collection of API responses

Source code in src/adpapi/client.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def call_endpoint(
    self,
    endpoint: str,
    select: list[str] | None = None,
    filters: str | FilterExpression | None = None,
    masked: bool = True,
    timeout: int = DEFAULT_TIMEOUT,
    page_size: int = 100,
    max_requests: int | None = None,
) -> list[dict]:
    """Call any Registered ADP Endpoint

    Args:
        endpoint (str): API Endpoint or qualified URL to call
        select (List[str]): Table Columns to pull
        masked (bool, optional): Mask Sensitive Columns Containing Personally Identifiable Information. Defaults to True.
        filters (str | FilterExpression, optional): OData Filter Expression. Strings will be passed directly,
            or OData strings can be automatically created from `adpapi.odata_filters.FilterExpression` objects
        timeout (int, optional): Time to wait on. Defaults to 30.
        page_size (int, optional): Amount of records to pull per API call (max 100). Defaults to 100.
        max_requests (Optional[int], optional): Maximum number of requests to make (for quick testing). Defaults to None.

    Raises:
        ValueError: When given an endpoint not following the call convention

    Returns:
        List[Dict]: The collection of API responses
    """

    # Request Cleanup and Validation Logic
    if page_size > 100:
        logger.warning("Page size > 100 not supported by API endpoint. Limiting to 100.")
        page_size = 100

    # Output/Request Initialization
    endpoint = self._clean_endpoint(endpoint)
    url = self.base_url + endpoint
    filter_param = self._handle_filters(filters)
    # Populate here instead of mutable default arguments
    if select is None:
        select = []
    select_param = ",".join(select)
    output = []
    skip = 0

    get_headers_fn = self.get_masked_headers if masked else self.get_unmasked_headers

    call_session = ApiSession(self.session, self.cert, get_headers_fn, timeout=timeout)

    params: dict[str, Any] = {"$top": page_size}
    if select_param:
        logging.debug(f"Restricting OData Selection to {select_param}")
        params["$select"] = select_param
    if filter_param:
        logging.debug(f"Filtering Results according to OData query: {filter_param}")
        params["$filter"] = filter_param

    while True:
        params["$skip"] = skip
        call_session.set_params(params)
        self._ensure_valid_token(timeout)
        response = call_session.get(url)

        if response.status_code == 204:
            logger.debug("End of pagination reached (204 No Content)")
            break

        try:
            data = response.json()
            output.append(data)

        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse JSON response: {e}")
            raise

        if max_requests is not None and len(output) >= max_requests:
            logger.debug(f"Max Requests reached: {max_requests}")
            break
        skip += page_size

    return output

Call a RestAPI Endpoint

Parameters:

Name Type Description Default
endpoint str

the endpoint path template (e.g. '/hr/workers/{workerId}')

required
method Optional[str]

the HTTP method to use for the request. Defaults to 'GET'.

'GET'
masked Optional[bool]

whether to use masked headers. Defaults to True.

True
timeout Optional[int]

the request timeout in seconds. Defaults to DEFAULT_TIMEOUT.

DEFAULT_TIMEOUT
params Optional[dict]

query parameters for the request. Defaults to None.

None
max_workers int

maximum number of threads for parallel requests. Defaults to 1 (sequential).

1
inject_path_params bool

when True, merge the resolved path parameters into each response dict. Useful when the API does not echo back identifiers (e.g. AOIDs) in the response body. Defaults to False.

False
**kwargs Any

path parameters to substitute into the endpoint template (e.g workerId=['123', '456']) - can be single values or lists of values for batch requests

{}

Raises: ValueError: if required path parameters are missing or if endpoint format is incorrect

Returns:

Type Description
list[dict]

List[Dict]: the collection of API responses for each substituted endpoint

Source code in src/adpapi/client.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def call_rest_endpoint(
    self,
    endpoint: str,
    method: str = "GET",
    masked: bool = True,
    timeout: int = DEFAULT_TIMEOUT,
    params: dict | None = None,
    max_workers: int = 1,
    inject_path_params: bool = False,
    **kwargs: Any,
) -> list[dict]:
    """Call a RestAPI Endpoint

    Args:
        endpoint (str): the endpoint path template (e.g. '/hr/workers/{workerId}')
        method (Optional[str], optional): the HTTP method to use for the request. Defaults to 'GET'.
        masked (Optional[bool], optional): whether to use masked headers. Defaults to True.
        timeout (Optional[int], optional): the request timeout in seconds. Defaults to DEFAULT_TIMEOUT.
        params (Optional[dict], optional): query parameters for the request. Defaults to None.
        max_workers (int, optional): maximum number of threads for parallel requests. Defaults to 1 (sequential).
        inject_path_params (bool, optional): when True, merge the resolved path parameters
            into each response dict. Useful when the API does not echo back identifiers
            (e.g. AOIDs) in the response body. Defaults to False.
        **kwargs: path parameters to substitute into the endpoint template (e.g workerId=['123', '456']) - can be single values or lists of values for batch requests
    Raises:
        ValueError: if required path parameters are missing or if endpoint format is incorrect

    Returns:
        List[Dict]: the collection of API responses for each substituted endpoint
    """
    is_valid, missing_params = validate_path_parameters(endpoint, kwargs)
    if not is_valid:
        raise ValueError(f"Missing required path parameters: {', '.join(missing_params)}")

    urls = substitute_path_parameters(endpoint, kwargs)
    if not urls:
        return []

    # Establish the call session
    get_headers_fn = self.get_masked_headers if masked else self.get_unmasked_headers

    call_session = ApiSession(self.session, self.cert, get_headers_fn, timeout=timeout)
    if params:
        call_session.set_params(params)

    # Ensure a valid token once before all requests to avoid race conditions
    # with concurrent threads each trying to refresh the token simultaneously.
    self._ensure_valid_token(timeout)

    def _fetch(url: str) -> dict:
        full_url = self.base_url + url
        response = call_session._request(url=full_url, method=RequestMethod(method))
        try:
            data = response.json()
            return data
        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse JSON response: {e}")
            raise

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        output = list(executor.map(_fetch, urls))

    if inject_path_params:
        param_sets = resolve_path_parameter_sets(endpoint, kwargs)
        for response, param_set in zip(output, param_sets, strict=False):
            response.update(param_set)

    return output

Filters

OData filter expressions for querying. Use FilterExpression.field() as the primary entry point:

from adpapi.odata_filters import FilterExpression

# Recommended: use FilterExpression.field()
filter = FilterExpression.field('fieldName').eq('targetValue')

# Advanced: construct a Field directly for reuse
from adpapi.odata_filters import Field
field = Field('fieldName')

See full details on supported OData operations:

Bases: Expr

Represents a field reference in an OData filter expression.

Fields are identified by their path (e.g., 'worker.person.firstName'). This class provides a fluent API for building filter conditions on fields.

Attributes:

Name Type Description
path str

The dot-separated path to the field, supporting nested properties.

Example

field = Field('worker.hireDate') field.eq('2020-01-01').to_odata() "(worker/hireDate eq '2020-01-01')"

Source code in src/adpapi/odata_filters.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
@dataclass(frozen=True)
class Field(Expr):
    """Represents a field reference in an OData filter expression.

    Fields are identified by their path (e.g., 'worker.person.firstName').
    This class provides a fluent API for building filter conditions on fields.

    Attributes:
        path (str): The dot-separated path to the field, supporting nested properties.

    Example:
        >>> field = Field('worker.hireDate')
        >>> field.eq('2020-01-01').to_odata()
        "(worker/hireDate eq '2020-01-01')"
    """

    path: str

    # comparisons
    def eq(self, val: Any) -> "BinaryOp":
        """Create an equality comparison filter (field = value).

        Args:
            val: The value to compare against. Can be string, number, boolean, or None.

        Returns:
            BinaryOp: A binary operation representing the equality condition.

        Example:
            >>> FilterExpression.field('status').eq('Active').to_odata()
            "(status eq 'Active')"
        """
        return BinaryOp(self, "eq", literal(val))

    def ne(self, val: Any) -> "BinaryOp":
        """Create a not-equal comparison filter (field != value).

        Args:
            val: The value to compare against. Can be string, number, boolean, or None.

        Returns:
            BinaryOp: A binary operation representing the not-equal condition.

        Example:
            >>> FilterExpression.field('status').ne('Inactive').to_odata()
            "(status ne 'Inactive')"
        """
        return BinaryOp(self, "ne", literal(val))

    def gt(self, val: Any) -> "BinaryOp":
        """Create a greater-than comparison filter (field > value).

        Args:
            val: The value to compare against. Typically a number or date string.

        Returns:
            BinaryOp: A binary operation representing the greater-than condition.

        Example:
            >>> FilterExpression.field('salary').gt(50000).to_odata()
            "(salary gt 50000)"
        """
        return BinaryOp(self, "gt", literal(val))

    def ge(self, val: Any) -> "BinaryOp":
        """Create a greater-than-or-equal comparison filter (field >= value).

        Args:
            val: The value to compare against. Typically a number or date string.

        Returns:
            BinaryOp: A binary operation representing the greater-than-or-equal condition.

        Example:
            >>> FilterExpression.field('hireDate').ge('2020-01-01').to_odata()
            "(hireDate ge '2020-01-01')"
        """
        return BinaryOp(self, "ge", literal(val))

    def lt(self, val: Any) -> "BinaryOp":
        """Create a less-than comparison filter (field < value).

        Args:
            val: The value to compare against. Typically a number or date string.

        Returns:
            BinaryOp: A binary operation representing the less-than condition.

        Example:
            >>> FilterExpression.field('salary').lt(100000).to_odata()
            "(salary lt 100000)"
        """
        return BinaryOp(self, "lt", literal(val))

    def le(self, val: Any) -> "BinaryOp":
        """Create a less-than-or-equal comparison filter (field <= value).

        Args:
            val: The value to compare against. Typically a number or date string.

        Returns:
            BinaryOp: A binary operation representing the less-than-or-equal condition.

        Example:
            >>> FilterExpression.field('retirementDate').le('2025-12-31').to_odata()
            "(retirementDate le '2025-12-31')"
        """
        return BinaryOp(self, "le", literal(val))

    # string functions
    def contains(self, val: Any) -> "Func":
        """Create a substring contains filter for string fields.

        Args:
            val: The substring to search for within the field value.

        Returns:
            Func: A function call representing the contains operation.

        Example:
            >>> FilterExpression.field('lastName').contains('Smith').to_odata()
            "contains(lastName, 'Smith')"
        """
        return Func("contains", [self, literal(val)])

    def startswith(self, val: Any) -> "Func":
        """Create a string starts-with filter.

        Args:
            val: The prefix to search for at the start of the field value.

        Returns:
            Func: A function call representing the startswith operation.

        Example:
            >>> FilterExpression.field('firstName').startswith('John').to_odata()
            "startswith(firstName, 'John')"
        """
        return Func("startswith", [self, literal(val)])

    def endswith(self, val: Any) -> "Func":
        """Create a string ends-with filter.

        Args:
            val: The suffix to search for at the end of the field value.

        Returns:
            Func: A function call representing the endswith operation.

        Example:
            >>> FilterExpression.field('email').endswith('@company.com').to_odata()
            "endswith(email, '@company.com')"
        """
        return Func("endswith", [self, literal(val)])

    # emulate IN as disjunction
    def isin(self, values: list[Any]) -> "Expr":
        """Create an IN filter for multiple values (field IN (val1, val2, ...)).

        Since OData v4 doesn't have a native IN operator, this is implemented as
        a series of OR conditions joined together.

        Args:
            values: A list of values to check against. If empty, returns false.

        Returns:
            Expr: An expression representing the IN operation. For empty lists,
                  returns an always-false condition (1 eq 0).

        Example:
            >>> statuses = ['Active', 'OnLeave', 'Pending']
            >>> FilterExpression.field('status').isin(statuses).to_odata()
            "((status eq 'Active') or ((status eq 'OnLeave') or (status eq 'Pending')))"
        """
        if not values:
            # empty IN -> false; represent as (1 eq 0)
            return BinaryOp(Literal(1), "eq", Literal(0))
        expr: Expr = BinaryOp(self, "eq", literal(values[0]))
        for v in values[1:]:
            clause = BinaryOp(self, "eq", literal(v))
            expr = BinaryOp(expr, "or", clause)
        return expr

    def to_odata(self) -> str:
        """Convert this field reference to an OData path string.

        Converts dot notation to forward slash notation for OData v4 compliance.

        Returns:
            str: The OData-compliant field path.

        Example:
            >>> Field('worker.person.firstName').to_odata()
            'worker/person/firstName'
        """
        # Convert dot notation to forward slash for OData v4 compliance
        # Input: "workers.workAssignments.reportsTo.positionID"
        # Output: "workers/workAssignments/reportsTo/positionID"
        return self.path.replace(".", "/")

contains(val)

Create a substring contains filter for string fields.

Parameters:

Name Type Description Default
val Any

The substring to search for within the field value.

required

Returns:

Name Type Description
Func Func

A function call representing the contains operation.

Example

FilterExpression.field('lastName').contains('Smith').to_odata() "contains(lastName, 'Smith')"

Source code in src/adpapi/odata_filters.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def contains(self, val: Any) -> "Func":
    """Create a substring contains filter for string fields.

    Args:
        val: The substring to search for within the field value.

    Returns:
        Func: A function call representing the contains operation.

    Example:
        >>> FilterExpression.field('lastName').contains('Smith').to_odata()
        "contains(lastName, 'Smith')"
    """
    return Func("contains", [self, literal(val)])

endswith(val)

Create a string ends-with filter.

Parameters:

Name Type Description Default
val Any

The suffix to search for at the end of the field value.

required

Returns:

Name Type Description
Func Func

A function call representing the endswith operation.

Example

FilterExpression.field('email').endswith('@company.com').to_odata() "endswith(email, '@company.com')"

Source code in src/adpapi/odata_filters.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def endswith(self, val: Any) -> "Func":
    """Create a string ends-with filter.

    Args:
        val: The suffix to search for at the end of the field value.

    Returns:
        Func: A function call representing the endswith operation.

    Example:
        >>> FilterExpression.field('email').endswith('@company.com').to_odata()
        "endswith(email, '@company.com')"
    """
    return Func("endswith", [self, literal(val)])

eq(val)

Create an equality comparison filter (field = value).

Parameters:

Name Type Description Default
val Any

The value to compare against. Can be string, number, boolean, or None.

required

Returns:

Name Type Description
BinaryOp BinaryOp

A binary operation representing the equality condition.

Example

FilterExpression.field('status').eq('Active').to_odata() "(status eq 'Active')"

Source code in src/adpapi/odata_filters.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def eq(self, val: Any) -> "BinaryOp":
    """Create an equality comparison filter (field = value).

    Args:
        val: The value to compare against. Can be string, number, boolean, or None.

    Returns:
        BinaryOp: A binary operation representing the equality condition.

    Example:
        >>> FilterExpression.field('status').eq('Active').to_odata()
        "(status eq 'Active')"
    """
    return BinaryOp(self, "eq", literal(val))

ge(val)

Create a greater-than-or-equal comparison filter (field >= value).

Parameters:

Name Type Description Default
val Any

The value to compare against. Typically a number or date string.

required

Returns:

Name Type Description
BinaryOp BinaryOp

A binary operation representing the greater-than-or-equal condition.

Example

FilterExpression.field('hireDate').ge('2020-01-01').to_odata() "(hireDate ge '2020-01-01')"

Source code in src/adpapi/odata_filters.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def ge(self, val: Any) -> "BinaryOp":
    """Create a greater-than-or-equal comparison filter (field >= value).

    Args:
        val: The value to compare against. Typically a number or date string.

    Returns:
        BinaryOp: A binary operation representing the greater-than-or-equal condition.

    Example:
        >>> FilterExpression.field('hireDate').ge('2020-01-01').to_odata()
        "(hireDate ge '2020-01-01')"
    """
    return BinaryOp(self, "ge", literal(val))

gt(val)

Create a greater-than comparison filter (field > value).

Parameters:

Name Type Description Default
val Any

The value to compare against. Typically a number or date string.

required

Returns:

Name Type Description
BinaryOp BinaryOp

A binary operation representing the greater-than condition.

Example

FilterExpression.field('salary').gt(50000).to_odata() "(salary gt 50000)"

Source code in src/adpapi/odata_filters.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def gt(self, val: Any) -> "BinaryOp":
    """Create a greater-than comparison filter (field > value).

    Args:
        val: The value to compare against. Typically a number or date string.

    Returns:
        BinaryOp: A binary operation representing the greater-than condition.

    Example:
        >>> FilterExpression.field('salary').gt(50000).to_odata()
        "(salary gt 50000)"
    """
    return BinaryOp(self, "gt", literal(val))

isin(values)

Create an IN filter for multiple values (field IN (val1, val2, ...)).

Since OData v4 doesn't have a native IN operator, this is implemented as a series of OR conditions joined together.

Parameters:

Name Type Description Default
values list[Any]

A list of values to check against. If empty, returns false.

required

Returns:

Name Type Description
Expr Expr

An expression representing the IN operation. For empty lists, returns an always-false condition (1 eq 0).

Example

statuses = ['Active', 'OnLeave', 'Pending'] FilterExpression.field('status').isin(statuses).to_odata() "((status eq 'Active') or ((status eq 'OnLeave') or (status eq 'Pending')))"

Source code in src/adpapi/odata_filters.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def isin(self, values: list[Any]) -> "Expr":
    """Create an IN filter for multiple values (field IN (val1, val2, ...)).

    Since OData v4 doesn't have a native IN operator, this is implemented as
    a series of OR conditions joined together.

    Args:
        values: A list of values to check against. If empty, returns false.

    Returns:
        Expr: An expression representing the IN operation. For empty lists,
              returns an always-false condition (1 eq 0).

    Example:
        >>> statuses = ['Active', 'OnLeave', 'Pending']
        >>> FilterExpression.field('status').isin(statuses).to_odata()
        "((status eq 'Active') or ((status eq 'OnLeave') or (status eq 'Pending')))"
    """
    if not values:
        # empty IN -> false; represent as (1 eq 0)
        return BinaryOp(Literal(1), "eq", Literal(0))
    expr: Expr = BinaryOp(self, "eq", literal(values[0]))
    for v in values[1:]:
        clause = BinaryOp(self, "eq", literal(v))
        expr = BinaryOp(expr, "or", clause)
    return expr

le(val)

Create a less-than-or-equal comparison filter (field <= value).

Parameters:

Name Type Description Default
val Any

The value to compare against. Typically a number or date string.

required

Returns:

Name Type Description
BinaryOp BinaryOp

A binary operation representing the less-than-or-equal condition.

Example

FilterExpression.field('retirementDate').le('2025-12-31').to_odata() "(retirementDate le '2025-12-31')"

Source code in src/adpapi/odata_filters.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def le(self, val: Any) -> "BinaryOp":
    """Create a less-than-or-equal comparison filter (field <= value).

    Args:
        val: The value to compare against. Typically a number or date string.

    Returns:
        BinaryOp: A binary operation representing the less-than-or-equal condition.

    Example:
        >>> FilterExpression.field('retirementDate').le('2025-12-31').to_odata()
        "(retirementDate le '2025-12-31')"
    """
    return BinaryOp(self, "le", literal(val))

lt(val)

Create a less-than comparison filter (field < value).

Parameters:

Name Type Description Default
val Any

The value to compare against. Typically a number or date string.

required

Returns:

Name Type Description
BinaryOp BinaryOp

A binary operation representing the less-than condition.

Example

FilterExpression.field('salary').lt(100000).to_odata() "(salary lt 100000)"

Source code in src/adpapi/odata_filters.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def lt(self, val: Any) -> "BinaryOp":
    """Create a less-than comparison filter (field < value).

    Args:
        val: The value to compare against. Typically a number or date string.

    Returns:
        BinaryOp: A binary operation representing the less-than condition.

    Example:
        >>> FilterExpression.field('salary').lt(100000).to_odata()
        "(salary lt 100000)"
    """
    return BinaryOp(self, "lt", literal(val))

ne(val)

Create a not-equal comparison filter (field != value).

Parameters:

Name Type Description Default
val Any

The value to compare against. Can be string, number, boolean, or None.

required

Returns:

Name Type Description
BinaryOp BinaryOp

A binary operation representing the not-equal condition.

Example

FilterExpression.field('status').ne('Inactive').to_odata() "(status ne 'Inactive')"

Source code in src/adpapi/odata_filters.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def ne(self, val: Any) -> "BinaryOp":
    """Create a not-equal comparison filter (field != value).

    Args:
        val: The value to compare against. Can be string, number, boolean, or None.

    Returns:
        BinaryOp: A binary operation representing the not-equal condition.

    Example:
        >>> FilterExpression.field('status').ne('Inactive').to_odata()
        "(status ne 'Inactive')"
    """
    return BinaryOp(self, "ne", literal(val))

startswith(val)

Create a string starts-with filter.

Parameters:

Name Type Description Default
val Any

The prefix to search for at the start of the field value.

required

Returns:

Name Type Description
Func Func

A function call representing the startswith operation.

Example

FilterExpression.field('firstName').startswith('John').to_odata() "startswith(firstName, 'John')"

Source code in src/adpapi/odata_filters.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def startswith(self, val: Any) -> "Func":
    """Create a string starts-with filter.

    Args:
        val: The prefix to search for at the start of the field value.

    Returns:
        Func: A function call representing the startswith operation.

    Example:
        >>> FilterExpression.field('firstName').startswith('John').to_odata()
        "startswith(firstName, 'John')"
    """
    return Func("startswith", [self, literal(val)])

to_odata()

Convert this field reference to an OData path string.

Converts dot notation to forward slash notation for OData v4 compliance.

Returns:

Name Type Description
str str

The OData-compliant field path.

Example

Field('worker.person.firstName').to_odata() 'worker/person/firstName'

Source code in src/adpapi/odata_filters.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def to_odata(self) -> str:
    """Convert this field reference to an OData path string.

    Converts dot notation to forward slash notation for OData v4 compliance.

    Returns:
        str: The OData-compliant field path.

    Example:
        >>> Field('worker.person.firstName').to_odata()
        'worker/person/firstName'
    """
    # Convert dot notation to forward slash for OData v4 compliance
    # Input: "workers.workAssignments.reportsTo.positionID"
    # Output: "workers/workAssignments/reportsTo/positionID"
    return self.path.replace(".", "/")

Logging

Quick basic logging configuration with a rotating file handler and stream handling.

Source code in src/adpapi/logger.py
10
11
12
13
14
15
16
17
18
19
20
21
def configure_logging():
    logging.basicConfig(
        filename="app.log",
        filemode="w",
        level=logging.DEBUG,
        format="%(asctime)s - %(levelname)s - %(message)s",
    )
    # Add console handler
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG)
    console_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
    logging.getLogger().addHandler(console_handler)