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 with any of the endpoint payload models provided in the endpoints module, such as SearchEndpoint.Params.

USAJobsClient

Represents a client connection to the USAJOBS REST API.

Source code in usajobsapi/client.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 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
class USAJobsClient:
    """Represents a client connection to the USAJOBS REST API."""

    def __init__(
        self,
        url: Optional[str] = "https://data.usajobs.gov",
        ssl_verify: bool = True,
        timeout: Optional[float] = 60,
        auth_user: Optional[str] = None,
        auth_key: Optional[str] = None,
        session: Optional[requests.Session] = None,
    ) -> None:
        """
        :param url: The URL of the USAJOBS REST API server, defaults to "https://data.usajobs.gov"
        :type url: str | None, optional
        :param ssl_verify: Whether SSL certificates should be validated, defaults to True
        :type ssl_verify: bool, optional
        :param timeout: Timeout to use for requests to the USA JOBS REST API, defaults to 60s
        :type timeout: float | None, optional
        :param auth_user: Email address associated with the API key, defaults to None
        :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: Session to reuse for HTTP connections, defaults to None
        :type session: requests.Session | None, optional
        """
        self._url = url

        # Timeout to use for requests to the server
        self.timeout = timeout

        # Whether SSL certificates should be validated
        self.ssl_verify = ssl_verify

        # Headers used for the Job Search API
        self.headers = {
            "Host": urlparse(self._url).hostname,
            "User-Agent": auth_user,
            "Authorization-Key": auth_key,
        }

        self._session = session or requests.Session()

    def _build_url(self, path: str) -> str:
        """Returns the full URL from the given path.

        If the path is already a URL, return it unchanged. If it is a path, append it to the stored base URL.

        :param path: A URL path.
        :type path: str
        :return: The full URL with the given path.
        :rtype: str
        """
        if path.startswith("http://") or path.startswith("https://"):
            return path
        return f"{self._url}{path}"

    def _request(
        self, method: str, path: str, params: Dict[str, str], add_auth: bool = False
    ) -> requests.Response:
        """Helper method for sending HTTP requests.

        :param method: HTTP method
        :type method: str
        :param path: Request URL
        :type path: str
        :param params: Request query parameters
        :type params: Dict[str, str]
        :param add_auth: If true, includes the stored authentication headers with the request, defaults to False
        :type add_auth: bool, optional
        :return: Request response
        :rtype: requests.Response
        """
        url = self._build_url(path)
        headers = self.headers if add_auth else {}

        # Send the request
        resp = self._session.request(
            method, url, params=params, headers=headers, timeout=self.timeout
        )
        resp.raise_for_status()
        return resp

    def announcement_text(
        self, **kwargs
    ) -> AnnouncementTextEndpoint.Response:  # pragma: no cover
        """Query the Announcement Text API.

        :return: Deserialized announcement text response
        :rtype: AnnouncementTextEndpoint.Response
        """
        raise NotImplementedError(
            "Announcement Text endpoint support is not yet implemented."
        )

    def search_jobs(self, **kwargs) -> SearchEndpoint.Response:
        """Query the [Job Search API](https://developer.usajobs.gov/api-reference/get-api-search).

        :return: Active job listings matching the search criteria.
        :rtype: SearchEndpoint.Response
        """
        params = SearchEndpoint.Params(**kwargs)
        resp = self._request(
            SearchEndpoint.model_fields["METHOD"].default,
            SearchEndpoint.model_fields["PATH"].default,
            params.to_params(),
            add_auth=True,
        )
        return SearchEndpoint.Response.model_validate(resp.json())

    def search_jobs_pages(self, **kwargs) -> Iterator[SearchEndpoint.Response]:
        """Yield Job Search pages, paginating to the next page.

        This can handle fresh requests or continue requests from a given page number.

        :yield: The response object for the given page number.
        :rtype: Iterator[SearchEndpoint.Response]
        """

        # Get page parameters by object name or alias
        page_number: Optional[int] = kwargs.pop("page", kwargs.pop("Page", None))
        results_per_page = kwargs.pop(
            "results_per_page", kwargs.pop("ResultsPerPage", None)
        )

        # If not provided, then start at the first page
        current_page: int = page_number or 1

        while True:
            call_kwargs = kwargs
            call_kwargs["page"] = current_page

            # results_per_page may not exist for the first loop iteration
            if results_per_page:
                call_kwargs["results_per_page"] = results_per_page

            # Query for the response object
            resp = self.search_jobs(**call_kwargs)
            yield resp

            # Break if no search_result object exists
            search_result = resp.search_result
            if not search_result:
                break

            # Break if there are no search_result.items
            page_results_count = search_result.result_count or len(search_result.items)
            if page_results_count <= 0:
                break

            # results_per_page may not exist for the first loop iteration
            # so set it to the length of the returned search_result.items
            if results_per_page is None:
                results_per_page = page_results_count

            # Break if there are no more pages
            total_result_count = search_result.result_total
            if (
                total_result_count
                and current_page * results_per_page >= total_result_count
            ):
                break

            # Break if the page is shorter than results_per_page
            if page_results_count < results_per_page:
                break

            current_page += 1

    def search_jobs_items(self, **kwargs) -> Iterator[SearchEndpoint.JOAItem]:
        """Yield Job Search job items, handling pagination as needed.

        :yield: The job summary item.
        :rtype: Iterator[SearchEndpoint.JOAItem]
        """
        for resp in self.search_jobs_pages(**kwargs):
            for item in resp.jobs():
                yield item

    def historic_joa(self, **kwargs) -> HistoricJoaEndpoint.Response:
        """Query the Historic JOAs API.

        :return: Deserialized historic job announcement response
        :rtype: HistoricJoaEndpoint.Response
        """
        params = HistoricJoaEndpoint.Params(**kwargs)
        resp = self._request(
            HistoricJoaEndpoint.model_fields["METHOD"].default,
            HistoricJoaEndpoint.model_fields["PATH"].default,
            params.to_params(),
        )
        return HistoricJoaEndpoint.Response.model_validate(resp.json())

    def historic_joa_pages(self, **kwargs) -> Iterator[HistoricJoaEndpoint.Response]:
        """Yield Historic JOA pages, following continuation tokens.

        This can handle fresh requests or continue from a response page with a continuation token.

        :raises RuntimeError: On a duplicate continuation token.
        :yield: The response object for the given continuation token.
        :rtype: Iterator[HistoricJoaEndpoint.Response]
        """

        # Get the token by object name or alias name
        token = kwargs.pop("continuationToken", kwargs.pop("continuation_token", None))

        seen_tokens: set[str] = set()
        if token:
            seen_tokens.add(token)

        while True:
            call_kwargs = kwargs
            if token:
                call_kwargs["continuation_token"] = token

            resp = self.historic_joa(**call_kwargs)

            next_token = resp.next_token()
            # Handle duplicate tokens
            if next_token and next_token in seen_tokens:
                raise RuntimeError(
                    f"Historic JOA pagination returned duplicate continuation token '{next_token}'"
                )

            yield resp

            # If more pages
            if not next_token:
                break

            seen_tokens.add(next_token)
            token = next_token

    def historic_joa_items(self, **kwargs) -> Iterator[HistoricJoaEndpoint.Item]:
        """Yield Historic JOA items, following continuation tokens.

        This can handle fresh requests or continue from a response page with a continuation token.

        :raises RuntimeError: On a duplicate continuation token.
        :yield: The response item.
        :rtype: Iterator[HistoricJoaEndpoint.Item]
        """
        token = kwargs.pop("continuationToken", kwargs.pop("continuation_token", None))

        seen_tokens: set[str] = set()
        if token:
            seen_tokens.add(token)

        while True:
            call_kwargs = kwargs
            if token:
                call_kwargs["continuation_token"] = token

            resp = self.historic_joa(**call_kwargs)

            for item in resp.data:
                yield item

            next_token = resp.next_token()
            # Handle duplicate tokens
            if next_token and next_token in seen_tokens:
                raise RuntimeError(
                    f"Historic JOA pagination returned duplicate continuation token '{next_token}'"
                )

            if not next_token:
                break

            seen_tokens.add(next_token)
            token = next_token

__init__(url='https://data.usajobs.gov', ssl_verify=True, timeout=60, auth_user=None, auth_key=None, session=None)

Parameters:
  • url (Optional[str], default: 'https://data.usajobs.gov' ) –

    The URL of the USAJOBS REST API server, defaults to "https://data.usajobs.gov"

  • ssl_verify (bool, default: True ) –

    Whether SSL certificates should be validated, defaults to True

  • timeout (Optional[float], default: 60 ) –

    Timeout to use for requests to the USA JOBS REST API, defaults to 60s

  • auth_user (Optional[str], default: None ) –

    Email address associated with the API key, defaults to None

  • auth_key (Optional[str], default: None ) –

    API key used for the Job Search API, defaults to None

  • session (Optional[Session], default: None ) –

    Session to reuse for HTTP connections, defaults to None

Source code in usajobsapi/client.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
def __init__(
    self,
    url: Optional[str] = "https://data.usajobs.gov",
    ssl_verify: bool = True,
    timeout: Optional[float] = 60,
    auth_user: Optional[str] = None,
    auth_key: Optional[str] = None,
    session: Optional[requests.Session] = None,
) -> None:
    """
    :param url: The URL of the USAJOBS REST API server, defaults to "https://data.usajobs.gov"
    :type url: str | None, optional
    :param ssl_verify: Whether SSL certificates should be validated, defaults to True
    :type ssl_verify: bool, optional
    :param timeout: Timeout to use for requests to the USA JOBS REST API, defaults to 60s
    :type timeout: float | None, optional
    :param auth_user: Email address associated with the API key, defaults to None
    :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: Session to reuse for HTTP connections, defaults to None
    :type session: requests.Session | None, optional
    """
    self._url = url

    # Timeout to use for requests to the server
    self.timeout = timeout

    # Whether SSL certificates should be validated
    self.ssl_verify = ssl_verify

    # Headers used for the Job Search API
    self.headers = {
        "Host": urlparse(self._url).hostname,
        "User-Agent": auth_user,
        "Authorization-Key": auth_key,
    }

    self._session = session or requests.Session()

announcement_text(**kwargs)

Query the Announcement Text API.

Returns:
  • AnnouncementTextEndpoint.Response

    Deserialized announcement text response

Source code in usajobsapi/client.py
105
106
107
108
109
110
111
112
113
114
115
def announcement_text(
    self, **kwargs
) -> AnnouncementTextEndpoint.Response:  # pragma: no cover
    """Query the Announcement Text API.

    :return: Deserialized announcement text response
    :rtype: AnnouncementTextEndpoint.Response
    """
    raise NotImplementedError(
        "Announcement Text endpoint support is not yet implemented."
    )

historic_joa(**kwargs)

Query the Historic JOAs API.

Returns:
  • HistoricJoaEndpoint.Response

    Deserialized historic job announcement response

Source code in usajobsapi/client.py
201
202
203
204
205
206
207
208
209
210
211
212
213
def historic_joa(self, **kwargs) -> HistoricJoaEndpoint.Response:
    """Query the Historic JOAs API.

    :return: Deserialized historic job announcement response
    :rtype: HistoricJoaEndpoint.Response
    """
    params = HistoricJoaEndpoint.Params(**kwargs)
    resp = self._request(
        HistoricJoaEndpoint.model_fields["METHOD"].default,
        HistoricJoaEndpoint.model_fields["PATH"].default,
        params.to_params(),
    )
    return HistoricJoaEndpoint.Response.model_validate(resp.json())

historic_joa_items(**kwargs)

Yield Historic JOA items, following continuation tokens.

This can handle fresh requests or continue from a response page with a continuation token.

:yield: The response item.

Raises:
  • RuntimeError

    On a duplicate continuation token.

Source code in usajobsapi/client.py
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
def historic_joa_items(self, **kwargs) -> Iterator[HistoricJoaEndpoint.Item]:
    """Yield Historic JOA items, following continuation tokens.

    This can handle fresh requests or continue from a response page with a continuation token.

    :raises RuntimeError: On a duplicate continuation token.
    :yield: The response item.
    :rtype: Iterator[HistoricJoaEndpoint.Item]
    """
    token = kwargs.pop("continuationToken", kwargs.pop("continuation_token", None))

    seen_tokens: set[str] = set()
    if token:
        seen_tokens.add(token)

    while True:
        call_kwargs = kwargs
        if token:
            call_kwargs["continuation_token"] = token

        resp = self.historic_joa(**call_kwargs)

        for item in resp.data:
            yield item

        next_token = resp.next_token()
        # Handle duplicate tokens
        if next_token and next_token in seen_tokens:
            raise RuntimeError(
                f"Historic JOA pagination returned duplicate continuation token '{next_token}'"
            )

        if not next_token:
            break

        seen_tokens.add(next_token)
        token = next_token

historic_joa_pages(**kwargs)

Yield Historic JOA pages, following continuation tokens.

This can handle fresh requests or continue from a response page with a continuation token.

:yield: The response object for the given continuation token.

Raises:
  • RuntimeError

    On a duplicate continuation token.

Source code in usajobsapi/client.py
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
def historic_joa_pages(self, **kwargs) -> Iterator[HistoricJoaEndpoint.Response]:
    """Yield Historic JOA pages, following continuation tokens.

    This can handle fresh requests or continue from a response page with a continuation token.

    :raises RuntimeError: On a duplicate continuation token.
    :yield: The response object for the given continuation token.
    :rtype: Iterator[HistoricJoaEndpoint.Response]
    """

    # Get the token by object name or alias name
    token = kwargs.pop("continuationToken", kwargs.pop("continuation_token", None))

    seen_tokens: set[str] = set()
    if token:
        seen_tokens.add(token)

    while True:
        call_kwargs = kwargs
        if token:
            call_kwargs["continuation_token"] = token

        resp = self.historic_joa(**call_kwargs)

        next_token = resp.next_token()
        # Handle duplicate tokens
        if next_token and next_token in seen_tokens:
            raise RuntimeError(
                f"Historic JOA pagination returned duplicate continuation token '{next_token}'"
            )

        yield resp

        # If more pages
        if not next_token:
            break

        seen_tokens.add(next_token)
        token = next_token

search_jobs(**kwargs)

Query the Job Search API.

Returns:
  • SearchEndpoint.Response

    Active job listings matching the search criteria.

Source code in usajobsapi/client.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def search_jobs(self, **kwargs) -> SearchEndpoint.Response:
    """Query the [Job Search API](https://developer.usajobs.gov/api-reference/get-api-search).

    :return: Active job listings matching the search criteria.
    :rtype: SearchEndpoint.Response
    """
    params = SearchEndpoint.Params(**kwargs)
    resp = self._request(
        SearchEndpoint.model_fields["METHOD"].default,
        SearchEndpoint.model_fields["PATH"].default,
        params.to_params(),
        add_auth=True,
    )
    return SearchEndpoint.Response.model_validate(resp.json())

search_jobs_items(**kwargs)

Yield Job Search job items, handling pagination as needed.

:yield: The job summary item.

Source code in usajobsapi/client.py
191
192
193
194
195
196
197
198
199
def search_jobs_items(self, **kwargs) -> Iterator[SearchEndpoint.JOAItem]:
    """Yield Job Search job items, handling pagination as needed.

    :yield: The job summary item.
    :rtype: Iterator[SearchEndpoint.JOAItem]
    """
    for resp in self.search_jobs_pages(**kwargs):
        for item in resp.jobs():
            yield item

search_jobs_pages(**kwargs)

Yield Job Search pages, paginating to the next page.

This can handle fresh requests or continue requests from a given page number.

:yield: The response object for the given page number.

Source code in usajobsapi/client.py
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
def search_jobs_pages(self, **kwargs) -> Iterator[SearchEndpoint.Response]:
    """Yield Job Search pages, paginating to the next page.

    This can handle fresh requests or continue requests from a given page number.

    :yield: The response object for the given page number.
    :rtype: Iterator[SearchEndpoint.Response]
    """

    # Get page parameters by object name or alias
    page_number: Optional[int] = kwargs.pop("page", kwargs.pop("Page", None))
    results_per_page = kwargs.pop(
        "results_per_page", kwargs.pop("ResultsPerPage", None)
    )

    # If not provided, then start at the first page
    current_page: int = page_number or 1

    while True:
        call_kwargs = kwargs
        call_kwargs["page"] = current_page

        # results_per_page may not exist for the first loop iteration
        if results_per_page:
            call_kwargs["results_per_page"] = results_per_page

        # Query for the response object
        resp = self.search_jobs(**call_kwargs)
        yield resp

        # Break if no search_result object exists
        search_result = resp.search_result
        if not search_result:
            break

        # Break if there are no search_result.items
        page_results_count = search_result.result_count or len(search_result.items)
        if page_results_count <= 0:
            break

        # results_per_page may not exist for the first loop iteration
        # so set it to the length of the returned search_result.items
        if results_per_page is None:
            results_per_page = page_results_count

        # Break if there are no more pages
        total_result_count = search_result.result_total
        if (
            total_result_count
            and current_page * results_per_page >= total_result_count
        ):
            break

        # Break if the page is shorter than results_per_page
        if page_results_count < results_per_page:
            break

        current_page += 1