Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/python-init/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ runs:
enable-cache: true

- name: Install the project
run: uv sync --locked --all-extras --dev
run: uv sync --locked --all-extras --all-groups
shell: bash
96 changes: 78 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,30 @@
[![Code Style][codestyle-img]][codestyle-lnk]
[![Coverage Status][codecov-img]][codecov-lnk]

A Python wrapper for the [USAJOBS REST API](https://developer.usajobs.gov/). The library aims to provide a simple interface for discovering and querying job postings from USAJOBS using Python.
`python-usajobsapi` is a typed Python wrapper for the [USAJOBS REST API](https://developer.usajobs.gov/). The project provides a clean interface for discovering and querying job postings using Python.

## Features
## Status

- Lightweight client for the USAJOBS REST API endpoints
- Leverage type hinting and validation for endpoint parameters
- Map endpoint results to Python objects
This project is under active development and its API may change. Changes to the [USAJOBS REST API documentation](https://developer.usajobs.gov/) are monitored and incorporated on a best-effort basis. Feedback and ideas are appreciated.

## Overview

The USAJOBS REST API exposes a large catalog of job opportunity announcements (JOAs) with a complex query surface. This project focuses on providing:

- Declarative endpoint definitions.
- Strongly typed request/query parameters and response models.
- Streaming helpers for paginating through large result sets.
- Normalization of data formats (e.g., date handling, booleans, payload serialization).

### Supported Endpoints

This package primarily aims to support searching and retrieval of active and past job listings. However, updates are planned to add support for all other documented endpoints.
This package primarily aims to support searching and retrieval of active and past job listings. Coverage of all [documented endpoints](https://developer.usajobs.gov/api-reference/) will continue to be expanded.

Currently, the following endpoints are supported:

- [Job Search API](https://developer.usajobs.gov/api-reference/get-api-search) (`/api/Search`)
- [Historic JOA API](https://developer.usajobs.gov/api-reference/get-api-historicjoa) (`/api/HistoricJoa`)
- Planned in [#6](https://github.com/paddy74/python-usajobsapi/issues/6) - [Announcement Text API](https://developer.usajobs.gov/api-reference/get-api-joa) (`/api/HistoricJoa/AnnouncementText`)

## Installation

Expand All @@ -45,37 +54,88 @@ cd python-usajobsapi
pip install .
```

## Usage
## Quickstart

Register for a USAJOBS API key and set a valid User-Agent before making requests.
1. [Request USAJOBS API credentials](https://developer.usajobs.gov/APIRequest/Forms/DeveloperSignup) (Job Search API only).
2. Instatiate the client (`USAJobsClient`) with your `User-Agent` (email) and API key.
3. Perform a search:

```python
from usajobsapi import USAJobsClient

client = USAJobsClient(auth_user="name@example.com", auth_key="YOUR_API_KEY")
results = client.search_jobs(keyword="data scientist", location_names=["Atlanta", "Georgia"]).search_result.jobs()
for job in results:
response = client.search_jobs(keyword="data scientist", location_names=["Atlanta", "Georgia"])

for job in response.jobs():
print(job.position_title)
```

### Pagination

### Handling pagination

Use streaming helpers to to iterate through multiple pages or individual result items without needing to worry about pagination:

```python
for job in client.search_jobs_items(keyword="cybersecurity", results_per_page=100):
if "Remote" in (job.position_location_display or ""):
print(job.position_title, job.organization_name)
```

## Developer Guide

Set up a development environment with [`uv`](https://docs.astral.sh/uv/):

```bash
uv sync --all-extras --all-groups
uv run pytest tests
uv run ruff check
uv run ruff format
```

### Key Development Principles

- Keep Pydantic models exhaustive and prefer descriptive field metadata so that auto-generated docs remain informative.
- Maintain 100% passing tests, at least 80% test coverage, formatting, and linting before opening a pull request.
- Update docstrings alongside code changes to keep the generated reference accurate.

### Document Generation

Documentation is generated using [MkDocs](https://www.mkdocs.org/). The technical reference surfaces the reStructuredText style docstrings from the package's source code.

```bash
uv sync --group docs

# Run the development server
uv run mkdocs serve -f mkdocs/mkdocs.yaml
# Build the static site
uv run mkdocs build -f mkdocs/mkdocs.yaml
```

## Contributing

Contributions are welcome! To get started:

1. Fork the repository and create a new branch.
2. Create a virtual environment and install development dependencies.
3. Run the test suite with `pytest` and ensure all tests pass.
4. Submit a pull request describing your changes.
2. Install development dependencies (see the [developer guide](#developer-guide)).
3. Add or update tests together with your change.
4. Run the full test, linting, and formatting suite locally.
5. Submit a pull request describing your changes and referencing any relevant issues.

Please open an issue first for major changes to discuss your proposal.
For major changes, open an issue first to discuss your proposal.

## License
## Design

Distributed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html). See [LICENSE](LICENSE) for details.
The software design architecture prioritizes composability and strong typing, ensuring that it is straightforward to add/update endpoints and generate documentation from docstrings.

## Project Status
- **Client session management**: `USAJobsClient` wraps a configurable `requests.Session` to reuse connections and centralize authentication headers.
- **Declarative endpoints**: Each USAJOBS endpoint is expressed as a Pydantic model with nested `Params` and `Response` classes, providing validation, serialization helpers, and rich metadata for documentation.
- **Pagination helpers**: Iterators (`search_jobs_pages` and `search_jobs_items`) encapsulate pagination logic and expose idiomatic Python iterators so users focus on data consumption, not page math.
- **Shared utilities**: Shared utilities handle API-specific normalization (e.g., date parsing, alias mapping) so endpoint models stay declarative and thin.

This project is under active development and its API may change. Changes to the [USAJOBS REST API documentation](https://developer.usajobs.gov/) shall be monitored and incorporated into this project in a reasonable amount of time. Feedback and ideas are appreciated.
## License

Distributed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html). See [LICENSE](LICENSE) for details.

## Contact

Expand Down
Empty file added mkdocs/docs/.gitkeep
Empty file.
17 changes: 0 additions & 17 deletions mkdocs/docs/index.md

This file was deleted.

8 changes: 3 additions & 5 deletions mkdocs/gen_ref_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
def gen_ref_pages(root_dir: Path, source_dir: Path, output_dir: str | Path) -> None:
"""Emit mkdocstrings-compatible reference pages and navigation entries.

:param root_dir: _description_
:param root_dir: Project root directory used to resolve edit links
:type root_dir: Path
:param source_dir: _description_
:param source_dir: Directory containing the Python packages to document
:type source_dir: Path
:param output_dir: Output directory for the generated files; must be relative (non-escaping) to the docs directory.
:type output_dir: str | Path
:raises ValueError: _description_
:raises ValueError: _description_
:raises ValueError: _description_
:raises ValueError: If `output_dir` is absolute, escapes the docs directory, or no Python modules are found
"""

# output_dir must be a relative, non-escaping path
Expand Down
10 changes: 8 additions & 2 deletions mkdocs/mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ nav:
- About:
- Changelog: about/changelog.md
- License: about/license.md
- Code of Conduct: about/conduct.md
- Security Policy: about/security.md

plugins:
- search
Expand All @@ -27,11 +29,15 @@ plugins:
- literate-nav:
nav_file: NAV_REF.md
- section-index
- mkdocstrings
- mkdocstrings:
handlers:
python:
selection:
docstring_style: sphinx
- external-files:
files:
- src: ../README.md
dest: README.md
dest: index.md
- src: ../CHANGELOG.md
dest: about/changelog.md
- src: ../LICENSE
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ packages = ["usajobsapi"]

[dependency-groups]
dev = [
"pre-commit>=4.3.0",
"pytest>=8.4.1",
"pytest-cov>=7.0.0",
"ruff>=0.12.11",
]
docs = [
"mkdocs-external-files",
"mkdocs-gen-files>=0.5.0",
"mkdocs-literate-nav>=0.6.2",
"mkdocs-section-index>=0.3.10",
"mkdocs>=1.6.1",
"mkdocstrings[python]>=0.30.1",
"pre-commit>=4.3.0",
"pytest>=8.4.1",
"pytest-cov>=7.0.0",
"ruff>=0.12.11",
]

[tool.semantic_release]
Expand Down
14 changes: 10 additions & 4 deletions usajobsapi/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
"""Wrapper for the USAJOBS REST API."""
"""
Wrapper for the USAJOBS REST API.

Use the high-level client to manage authentication headers and make strongly typed requests to individual endpoints. The models documented below are auto-generated from the runtime code so every parameter, default, and helper stays in sync with the library.

To execute a query, pair the [`USAJobsClient`][usajobsapi.client.USAJobsClient] with any of the endpoint payload models provided in the [`endpoints` module][usajobsapi.endpoints], such as [`SearchEndpoint.Params`][usajobsapi.endpoints.search.SearchEndpoint.Params].
"""

from collections.abc import Iterator
from typing import Dict, Optional
Expand Down Expand Up @@ -36,7 +42,7 @@ def __init__(
:type auth_user: str | None, optional
:param auth_key: API key used for the Job Search API, defaults to None
:type auth_key: str | None, optional
:param session: _description_, defaults to None
:param session: Session to reuse for HTTP connections, defaults to None
:type session: requests.Session | None, optional
"""
self._url = url
Expand Down Expand Up @@ -99,7 +105,7 @@ def _request(
def announcement_text(self, **kwargs) -> AnnouncementTextEndpoint.Response:
"""Query the Announcement Text API.

:return: _description_
:return: Deserialized announcement text response
:rtype: AnnouncementTextEndpoint.Response
"""
params = AnnouncementTextEndpoint.Params(**kwargs)
Expand Down Expand Up @@ -197,7 +203,7 @@ def search_jobs_items(self, **kwargs) -> Iterator[SearchEndpoint.JOAItem]:
def historic_joa(self, **kwargs) -> HistoricJoaEndpoint.Response:
"""Query the Historic JOAs API.

:return: _description_
:return: Deserialized historic job announcement response
:rtype: HistoricJoaEndpoint.Response
"""
params = HistoricJoaEndpoint.Params(**kwargs)
Expand Down
6 changes: 5 additions & 1 deletion usajobsapi/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Wrapper for USAJOBS REST API endpoints."""
"""
Wrapper for USAJOBS REST API endpoints.

Each endpoint exposes declarative `Params` and `Response` models so you can validate queries and parse responses without hand-written schemas.
"""

from .announcementtext import AnnouncementTextEndpoint
from .historicjoa import HistoricJoaEndpoint
Expand Down
8 changes: 7 additions & 1 deletion usajobsapi/endpoints/announcementtext.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
"""Wrapper for the Announcement Text API."""
"""
Wrapper for the Announcement Text API.

Fetch the rendered job announcement text for a single job opportunity announcement.

Pair this endpoint with a control number discovered from [`SearchEndpoint.JOAItem`][usajobsapi.endpoints.search.SearchEndpoint.JOAItem] to pull the full HTML description.
"""

from typing import Dict

Expand Down
10 changes: 9 additions & 1 deletion usajobsapi/endpoints/historicjoa.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
"""Wrapper for the Historic JOAs API."""
"""
Wrapper for the Historic JOAs API.

Access archived job opportunity announcements with date filters, control numbers, and hiring-organization metadata.

- Feed a control number captured from a [search result item's `id`][usajobsapi.endpoints.search.SearchEndpoint.JOAItem] into [`usajobs_control_numbers`][usajobsapi.endpoints.historicjoa.HistoricJoaEndpoint.Params] to retrieve historical records for the same posting.
- Date filters such as [`start_position_open_date`][usajobsapi.endpoints.historicjoa.HistoricJoaEndpoint.Params] normalize strings as `datetime.date` objects and are reflected back in a response's `position_open_date`.
- Boolean indicators rely on normalization validators to handle the API's inconsistent input/output formats for booleans.
"""

import datetime as dt
from typing import Dict, List, Optional
Expand Down
14 changes: 12 additions & 2 deletions usajobsapi/endpoints/search.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
"""Wrapper for the Job Search API."""
"""
Wrapper for the Job Search API.

The search endpoint wraps the core USAJOBS Job Search API. Enumerations describe allowed
query values, the nested [`Params`][usajobsapi.endpoints.search.SearchEndpoint.Params] model validates input, and the response graph mirrors
the payload returned from the API.

- Filter inputs such as [`hiring_path`][usajobsapi.endpoints.search.SearchEndpoint.Params] and [`pay_grade_high`][usajobsapi.endpoints.search.SearchEndpoint.Params] are surfaced in a [search result's `params` field][usajobsapi.endpoints.search.SearchEndpoint.Response] so you can inspect what was sent to USAJOBS.
- Each [`JOAItem`][usajobsapi.endpoints.search.SearchEndpoint.JOAItem] contains a [`JOADescriptor`][usajobsapi.endpoints.search.SearchEndpoint.JOADescriptor] with rich metadata (for example, [`PositionRemuneration`][usajobsapi.endpoints.search.SearchEndpoint.PositionRemuneration]) that aligns with the salary filter parameters.
- [`SearchEndpoint.Response.jobs`][usajobsapi.endpoints.search.SearchEndpoint.Response.jobs] returns the flattened list of [`JOAItem`][usajobsapi.endpoints.search.SearchEndpoint.JOAItem] instances that correspond to the [`items`][usajobsapi.endpoints.search.SearchEndpoint.SearchResult] in the response payload.
"""

from __future__ import annotations

Expand Down Expand Up @@ -377,7 +387,7 @@ def _radius_requires_location(self) -> "SearchEndpoint.Params":
def _check_min_le_max(
cls, v: Optional[int], info: ValidationInfo
) -> Optional[int]:
"""Validate that renumeration max is >= renumeration min."""
"""Validate that remuneration max is >= remuneration min."""
mn = info.data.get("remuneration_min")
if v is not None and mn is not None and v < mn:
raise ValueError(
Expand Down
22 changes: 13 additions & 9 deletions usajobsapi/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Helper utility functions."""
"""
Helper utility functions.

Shared helpers keep endpoint payloads consistent and ergonomic. These utilities handle normalization, serialization, and data validation used throughout the client and endpoint models.
"""

import datetime as dt
from enum import Enum
Expand Down Expand Up @@ -27,7 +31,7 @@ def _normalize_date(value: None | dt.datetime | dt.date | str) -> Optional[dt.da


def _normalize_yn_bool(value: None | bool | str) -> Optional[bool]:
"""Normalize "Y"/"N" to `bool`."""
"""Normalize `"Y"`/`"N"` to `bool`."""

if value is None:
return None
Expand Down Expand Up @@ -80,9 +84,9 @@ def _normalize_param(value: Any) -> Optional[str]:
def _dump_by_alias(model: BaseModel) -> Dict[str, str]:
"""Dump a Pydantic model to a query-param dict using the model's field aliases and USAJOBS formatting rules (lists + bools).

:param model: _description_
:param model: Pydantic model instance to export using field aliases
:type model: BaseModel
:return: _description_
:return: Mapping of alias names to normalized parameter values
:rtype: Dict[str, str]
"""
# Use the API's wire names and drop `None`s
Expand All @@ -102,13 +106,13 @@ def _is_inrange(n: int | float, lower: int | float, upper: int | float):

A closed interval [a, b] represents the set of all real numbers greater or equal to a and less or equal to b.

:param n: _description_
:param n: Value to check
:type n: int
:param lower: _description_
:param lower: Lower bound of the interval
:type lower: int
:param upper: _description_
:param upper: Upper bound of the interval
:type upper: int
:return: _description_
:rtype: _type_
:return: `True` if the value falls inside the closed interval `[lower, upper]`
:rtype: bool
"""
return n >= lower and n <= upper
Loading