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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,58 @@ else:
)
```

### Getting Job Posting Data

#### By Search (Elasticsearch)

```python
es_query = {
"query": {
"bool": {
"must": [
{"term": {"title_role": "engineering"}},
{"term": {"remote_work_policy": "remote"}},
]
}
}
}
data = {
"query": es_query,
"size": 10,
"pretty": True,
}
result = client.job_posting.search(**data)
if result.ok:
print(result.text)
else:
print(
f"Status: {result.status_code}"
f"\nReason: {result.reason}"
f"\nMessage: {result.json()['error']['message']}"
)
```

#### By Search (Field Parameters)

```python
data = {
"title_role": "engineering",
"remote_work_policy": "remote",
"is_active": True,
"size": 10,
"pretty": True,
}
result = client.job_posting.search(**data)
if result.ok:
print(result.text)
else:
print(
f"Status: {result.status_code}"
f"\nReason: {result.reason}"
f"\nMessage: {result.json()['error']['message']}"
)
```

### Using supporting APIs

#### Get Autocomplete Suggestions
Expand Down Expand Up @@ -449,6 +501,12 @@ PDLPY(sandbox=True)
| [Person Identify API](https://docs.peopledatalabs.com/docs/identify-api) | `PDLPY.person.identify(**params)` |
| [Person Changelog API](https://docs.peopledatalabs.com/docs/person-changelog-api) | `PDLPY.person.changelog(**params)` |

**Job Posting Endpoints**

| API Endpoint | PDLPY Function |
| ----------------------------------------------------------------------------------------------- | --------------------------------------- |
| [Job Posting Search API](https://docs.peopledatalabs.com/docs/job-posting-search-api) | `PDLPY.job_posting.search(**params)` |

**Company Endpoints**

| API Endpoint | PDLPY Function |
Expand Down
41 changes: 41 additions & 0 deletions src/peopledatalabs/endpoints/job_posting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Defines all API endpoints for the 'Job Posting' section.
"""

from pydantic.v1.dataclasses import dataclass

from . import Endpoint
from ..models import job_posting as job_posting_models
from ..logger import get_logger


logger = get_logger("endpoints.job_posting")


@dataclass
class JobPosting(Endpoint):
"""
Class for all APIs of "job_posting" type.
"""

section: str = "job_posting"

def search(self, **kwargs):
"""
Calls PeopleDataLabs' job_posting/search API.

Dispatches to the Elasticsearch-style validator when 'query' is
provided, and to the field-based validator otherwise.

Args:
**kwargs: Parameters for the API as defined
in the documentation.

Returns:
A requests.Response object with the result of the HTTP call.
"""
if "query" in kwargs:
model = job_posting_models.JobPostingQuerySearchModel
else:
model = job_posting_models.JobPostingParamSearchModel
return self._search(model, **kwargs)
8 changes: 8 additions & 0 deletions src/peopledatalabs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .endpoints import Endpoint
from .endpoints.person import Person
from .endpoints.company import Company
from .endpoints.job_posting import JobPosting
from .endpoints.location import Location
from .endpoints.school import School
from .logger import get_logger
Expand Down Expand Up @@ -177,3 +178,10 @@ def person(self):
Calls API from the person section.
"""
return Person(self.api_key, self.base_path)

@property
def job_posting(self):
"""
Calls API from the job_posting section.
"""
return JobPosting(self.api_key, self.base_path)
89 changes: 89 additions & 0 deletions src/peopledatalabs/models/job_posting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Models for input parameters of the Job Posting APIs.
"""

from datetime import date
from enum import Enum
from typing import Optional

from pydantic.v1 import (
BaseModel,
conint,
)


class RemoteWorkPolicy(str, Enum):
"""
Valid values for 'remote_work_policy' on job_posting search.
"""

remote = "remote"
onsite = "onsite"


class SalaryPeriod(str, Enum):
"""
Valid values for 'salary_period' on job_posting search.
"""

year = "year"
month = "month"
week = "week"
day = "day"
hour = "hour"


class JobPostingQuerySearchModel(BaseModel):
"""
Validator model for the job_posting search API when using an Elasticsearch-
style query body.
"""

query: dict
size: Optional[conint(ge=1, le=100)] = 10
pretty: Optional[bool] = False
scroll_token: Optional[str] = None


class JobPostingParamSearchModel(BaseModel):
"""
Validator model for the job_posting search API when using the field-based
parameter form (no 'query' body).
"""

id: Optional[str]
first_seen_min: Optional[date]
first_seen_max: Optional[date]
deactivated_date_min: Optional[date]
deactivated_date_max: Optional[date]

title: Optional[str]
title_class: Optional[str]
title_role: Optional[str]
title_sub_role: Optional[str]
title_levels: Optional[str]

company_id: Optional[str]
company_name: Optional[str]
company_industry: Optional[str]
company_industry_v2: Optional[str]
company_website: Optional[str]
company_profile: Optional[str]

location: Optional[str]
description: Optional[str]

salary_range_min: Optional[int]
salary_range_max: Optional[int]
salary_currency: Optional[str]
salary_period: Optional[SalaryPeriod]

remote_work_policy: Optional[RemoteWorkPolicy]
inferred_skills: Optional[str]
last_verified_min: Optional[date]
last_verified_max: Optional[date]

is_active: Optional[bool]
size: Optional[conint(ge=1, le=100)] = 10
pretty: Optional[bool] = False
scroll_token: Optional[str] = None
Empty file added tests/job_posting/__init__.py
Empty file.
Empty file.
132 changes: 132 additions & 0 deletions tests/job_posting/endpoints/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
Tests calls to the job_posting/search API.
"""

import logging

import pytest
from pydantic.v1 import ValidationError
import requests

from peopledatalabs.errors import EmptyParametersException
from peopledatalabs.models.job_posting import (
JobPostingParamSearchModel,
JobPostingQuerySearchModel,
)


logging.basicConfig()
logger = logging.getLogger("PeopleDataLabs.tests.job_posting.search")


@pytest.mark.usefixtures("client_with_fake_api_key")
def test_search_empty_params_throw_error(client_with_fake_api_key):
"""
Tests calling the search method without parameters.

Should raise EmptyParametersException.
"""
with pytest.raises(EmptyParametersException):
client_with_fake_api_key.job_posting.search()


def test_param_model_size_out_of_range_raises_validation_error():
"""
Param-mode size must be between 1 and 100.
"""
with pytest.raises(ValidationError):
JobPostingParamSearchModel(title="engineer", size=0)
with pytest.raises(ValidationError):
JobPostingParamSearchModel(title="engineer", size=101)


def test_query_model_size_out_of_range_raises_validation_error():
"""
Query-mode size must also be between 1 and 100.
"""
with pytest.raises(ValidationError):
JobPostingQuerySearchModel(query={"match_all": {}}, size=0)
with pytest.raises(ValidationError):
JobPostingQuerySearchModel(query={"match_all": {}}, size=101)


def test_param_model_invalid_remote_work_policy_raises_validation_error():
"""
remote_work_policy must be one of the enum values.
"""
with pytest.raises(ValidationError):
JobPostingParamSearchModel(remote_work_policy="hybrid")


def test_param_model_invalid_salary_period_raises_validation_error():
"""
salary_period must be one of the enum values.
"""
with pytest.raises(ValidationError):
JobPostingParamSearchModel(salary_period="fortnight")


def test_param_model_is_active_omitted_by_default():
"""
is_active is an opt-in filter and must not be sent unless the caller sets
it explicitly.
"""
model = JobPostingParamSearchModel(title="engineer")
assert "is_active" not in model.dict(exclude_none=True)


def test_scroll_token_round_trips_as_opaque_string():
"""
scroll_token is the opaque base64 token returned by the API and must be
passed back unchanged on subsequent calls.
"""
token = "eyJhIjogMX0="
query_model = JobPostingQuerySearchModel(
query={"match_all": {}}, scroll_token=token
)
assert query_model.scroll_token == token

param_model = JobPostingParamSearchModel(
title="engineer", scroll_token=token
)
assert param_model.scroll_token == token


@pytest.mark.usefixtures("client")
def test_api_endpoint_search_query(client):
"""
Tests successful execution of search API by ES query.
"""
es_query = {
"query": {
"bool": {
"must": [
{"term": {"title_role": "engineering"}},
]
}
}
}
data = {
"query": es_query,
"size": 10,
"pretty": True,
}
response = client.job_posting.search(**data)
assert isinstance(response, requests.Response)
assert response.status_code == 200


@pytest.mark.usefixtures("client")
def test_api_endpoint_search_params(client):
"""
Tests successful execution of search API by field parameters.
"""
data = {
"title_role": "engineering",
"remote_work_policy": "remote",
"size": 10,
"pretty": True,
}
response = client.job_posting.search(**data)
assert isinstance(response, requests.Response)
assert response.status_code == 200
2 changes: 1 addition & 1 deletion tests/person/endpoints/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_api_endpoint_changelog(client):
Tests successful execution of changelog API.
"""
changelog = client.person.changelog(
current_version="32.0", origin_version="31.2", type="updated"
current_version="33.0", origin_version="32.0", type="updated"
)
assert isinstance(changelog, requests.Response)
assert changelog.status_code == 200
Loading