Skip to content

Commit

Permalink
fix: update Flatchr integration to accommodate API limitations
Browse files Browse the repository at this point in the history
  • Loading branch information
itsnedhir committed Dec 16, 2024
1 parent aaa336a commit e54f5b3
Show file tree
Hide file tree
Showing 21 changed files with 390 additions and 139 deletions.
243 changes: 163 additions & 80 deletions manifest.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/hrflow_connectors/v2/connectors/flatchr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This new connector will enable:
- ➡️ Send Profiles data from a Source of your choice to Flatchr.
- ⬅️ Send Jobs data from Flatchr to a Destination of your choice.
- ➡️ Send Jobs data from a Source of your choice to Flatchr.

<p align="center">
<image src=https://github.com/user-attachments/assets/38d23ec9-ab19-4f6f-8691-c9c8941e30de
width=90% height=100% >
Expand Down Expand Up @@ -75,7 +76,6 @@ To browse the examples of actions corresponding to released versions of 🤗 thi
</p>



Once the connector module is imported, you can leverage all the different actions that it offers.

For more code details checkout connector code.
Expand Down
155 changes: 129 additions & 26 deletions src/hrflow_connectors/v2/connectors/flatchr/aisles.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ class FlatchrBaseURL(str, Enum):


class AuthParameters(Struct):
api_key = Annotated[
api_key: Annotated[
str,
Meta(
description="The API key to authenticate with the Flatchr API",
),
]
company_id = Annotated[
company_id: Annotated[
str,
Meta(
description="The ID of the company to authenticate with",
Expand All @@ -51,7 +51,7 @@ class AuthParameters(Struct):
] = FlatchrBaseURL.PRODUCTION


class ReadProfilesParameters(Struct):
class ReadProfilesParameters(Struct, omit_defaults=True):
firstname: Annotated[
t.Optional[str],
Meta(
Expand Down Expand Up @@ -96,12 +96,6 @@ class ReadProfilesParameters(Struct):
description="The end date in MM/DD/YY of the search",
),
] = None
company: Annotated[
t.Optional[str],
Meta(
description="Allows a search on several companies for multi-accounts",
),
] = None
vacancy: Annotated[
t.Optional[str],
Meta(description="id of the offer in which the candidate is involved"),
Expand Down Expand Up @@ -131,14 +125,14 @@ class UpdateProfilesParameters(Struct):

class ArchiveProfilesParameters(Struct):
vacancy_id: Annotated[
str,
t.Optional[str],
Meta(
description=(
"The ID of the offer to assign the candidate to\nEquivalent to id in"
" the Flatchr API not vacancy_id nor the slug"
),
),
]
] = None


class ReadJobsParameters(Struct):
Expand Down Expand Up @@ -171,8 +165,96 @@ def read_profiles(
)
raise Exception("Failed to fetch profiles from Flatchr")
profiles = response.json()
# The following steps are a workaround around limitations and misconfigurations in the Flatchr API
# We go through the collumns endpoint to get the id of the column the candidate is in right now using the column name
columns_response = requests.get(
"{base_url}/company/{companyId}/columns".format(
base_url=auth_parameters.env_base_url, companyId=auth_parameters.company_id
),
headers=headers,
)
columns = columns_response.json()
# we go through the vacancies endpoint to get the id of the vacancy the candidate is attached to using the vacancy_id in the profile
vacancies_response = requests.get(
"{base_url}/company/{companyId}/vacancies".format(
base_url=auth_parameters.env_base_url, companyId=auth_parameters.company_id
),
headers=headers,
)
vacancies = vacancies_response.json()

for profile in profiles:
yield profile
column_id = next(
(
column["id"]
for column in columns
if column["title"] == profile["column"]
),
None,
)
vacancy_id = next(
(
vacancy["id"]
for vacancy in vacancies
if vacancy["vacancy_id"] == profile["vacancy_id"]
),
None,
)
# We move candidate to the same column he's in right now just to get the candidate id
if column_id and vacancy_id:
move_candidate_response = requests.put(
"{base_url}/company/{companyId}/vacancy/{vacancyId}/applicant/{applicantId}"
.format(
base_url=auth_parameters.env_base_url,
companyId=auth_parameters.company_id,
vacancyId=vacancy_id,
applicantId=profile["applicant"],
),
headers=headers,
json={"column_id": column_id},
)
if move_candidate_response.status_code // 100 != 2:
adapter.error(
"Failed to move candidate in Flatchr status_code={} response={}"
.format(
move_candidate_response.status_code,
move_candidate_response.text,
)
)
candidate_additional_info = move_candidate_response.json()["candidate"]
# Get the candidate resume
candidate_resume_response = requests.get(
"{base_url}/company/{companyId}/files/expire?key={key}&extension={extension}&token={token}"
.format(
base_url=auth_parameters.env_base_url,
companyId=auth_parameters.company_id,
key=candidate_additional_info["aws"]["key"],
extension=candidate_additional_info["aws"]["extension"],
token=auth_parameters.api_key,
)
)
if candidate_resume_response.status_code // 100 != 2:
adapter.error(
"Failed to get candidate resume in Flatchr status_code={}"
" response={}".format(
candidate_resume_response.status_code,
candidate_resume_response.text,
)
)
candidate_additional_info["resume"] = None
else:
candidate_additional_info["resume"] = {
"raw": candidate_resume_response.content,
"content_type": candidate_additional_info["aws"]["extension"],
}
profile.update(candidate_additional_info)
yield profile
else:
adapter.error(
"Failed to get column id or vacancy id for candidate column_id={} "
"vacancy_id={}".format(column_id, vacancy_id)
)
raise Exception("Failed to get column id or vacancy id for candidate")


def write(
Expand Down Expand Up @@ -215,24 +297,47 @@ def archive(
items: t.Iterable[dict],
) -> t.List[t.Dict]:
failed_profiles = []
FLATCHR_ARCHIVE_CANDIDATES_ENDPOINT = (
"{base_url}/company/{companyId}/vacancy/{vacancyId}/applicant".format(
base_url=auth_parameters.env_base_url,
companyId=auth_parameters.company_id,
vacancyId=parameters.vacancy_id,
)
)
headers = {"Authorization": "Bearer {}".format(auth_parameters.api_key)}

for profile in items:
response = requests.delete(
url=f"{FLATCHR_ARCHIVE_CANDIDATES_ENDPOINT}/{profile['applicant']}",
if parameters.vacancy_id:
vacancy_id = parameters.vacancy_id
else:
# we go through the vacancies endpoint to get the id of the vacancy the candidate is attached to using the vacancy_id in the profile
vacancies_response = requests.get(
"{base_url}/company/{companyId}/vacancies".format(
base_url=auth_parameters.env_base_url,
companyId=auth_parameters.company_id,
),
headers=headers,
)
vacancies = vacancies_response.json()
vacancy_id = next(
(
vacancy["id"]
for vacancy in vacancies
if vacancy["vacancy_id"] == profile["vacancy_id"]
),
None,
)

archive_candidate_response = requests.delete(
"{base_url}/company/{companyId}/vacancy/{vacancyId}/applicant/{applicantId}"
.format(
base_url=auth_parameters.env_base_url,
companyId=auth_parameters.company_id,
vacancyId=vacancy_id,
applicantId=profile["applicant_id"],
),
headers=headers,
)
if response.status_code // 100 != 2:
if archive_candidate_response.status_code // 100 != 2:
adapter.error(
"Failed to archive profile in Flatchr status_code={} response={}"
.format(response.status_code, response.text)
.format(
archive_candidate_response.status_code,
archive_candidate_response.text,
)
)
failed_profiles.append(profile)

Expand All @@ -254,9 +359,7 @@ def update(
headers = {"Authorization": "Bearer {}".format(auth_parameters.api_key)}

for profile in items:
json_data = dict(
reference=profile.pop("email"), type="applicants", value=profile
)
json_data = dict(reference=profile.pop("id"), type="applicants", value=profile)
response = requests.post(
url=FLATCHR_UPDATE_CANDIDATES_ENDPOINT,
headers=headers,
Expand Down
45 changes: 33 additions & 12 deletions src/hrflow_connectors/v2/connectors/flatchr/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,31 @@

def format_flatchr_profile(flatchr_profile: t.Dict) -> t.Dict:
hrflow_profile = dict(
reference=flatchr_profile.get("applicant"),
reference=flatchr_profile.get("id"),
created_at=flatchr_profile.get("created_at"),
info=dict(
email=flatchr_profile.get("email"),
first_name=flatchr_profile.get("firstname"),
last_name=flatchr_profile.get("lastname"),
full_name=(
f"{flatchr_profile.get('firstname')} {flatchr_profile.get('lastname')}"
),
location=dict(text="", lat=None, lng=None),
phone=flatchr_profile.get("phone"),
),
educations=[],
experiences=[],
attachments=[],
tags=[
{"name": "applicant_id", "value": flatchr_profile.get("applicant")},
{"name": "vacancy", "value": flatchr_profile.get("vacancy")},
{"name": "vacancy_id", "value": flatchr_profile.get("vacancy")},
{"name": "vacancy_id", "value": flatchr_profile.get("vacancy_id")},
{"name": "source", "value": flatchr_profile.get("source")},
{"name": "external_id", "value": flatchr_profile.get("external_id")},
{"name": "hired", "value": flatchr_profile.get("hired")},
{"name": "column", "value": flatchr_profile.get("column")},
],
resume=flatchr_profile.get("resume"),
)
return hrflow_profile

Expand Down Expand Up @@ -56,20 +61,36 @@ def format_hrflow_profile(hrflow_profile: t.Dict) -> t.Dict:
return profile


def format_hrflow_profile_for_update(hrflow_profile: t.Dict) -> t.Dict:
flatchr_profile = format_hrflow_profile(hrflow_profile)
flatchr_profile["id"] = hrflow_profile["reference"]
return flatchr_profile


def format_flatchr_profile_for_archive(flatchr_profile: t.Dict) -> t.Dict:
return {"reference": flatchr_profile["applicant"]}
return {"reference": flatchr_profile["id"]}


def format_hrflow_for_archive(hrflow_profile: t.Dict) -> t.Dict:
return {"applicant": hrflow_profile["reference"]}
def format_hrflow_profile_for_archive(hrflow_profile: t.Dict) -> t.Dict:
vacancy_id = next(
(tag["value"] for tag in hrflow_profile["tags"] if tag["name"] == "vacancy_id"),
None,
)
applicant_id = next(
(
tag["value"]
for tag in hrflow_profile["tags"]
if tag["name"] == "applicant_id"
),
None,
)
return {"applicant_id": applicant_id, "vacancy_id": vacancy_id}


def get_tags(flatchr_job: t.Dict) -> t.List:
t = lambda name, value: dict(name=name, value=value)
tags = [
t("id", flatchr_job.get("id")),
t("slug", flatchr_job.get("slug")),
t("reference", flatchr_job.get("reference")),
t("status", flatchr_job.get("status")),
t("language", flatchr_job.get("language")),
t("company_id", flatchr_job.get("company_id")),
Expand All @@ -87,7 +108,7 @@ def get_tags(flatchr_job: t.Dict) -> t.List:

def format_flatchr_job(flatchr_job: t.Dict) -> t.Dict:
hrflow_job = dict(
reference=flatchr_job.get("vacancy_id"),
reference=flatchr_job.get("id"),
name=flatchr_job.get("title"),
created_at=flatchr_job.get("created_at"),
updated_at=flatchr_job.get("updated_at"),
Expand Down Expand Up @@ -115,15 +136,15 @@ def format_flatchr_job(flatchr_job: t.Dict) -> t.Dict:
url=flatchr_job.get("apply_url"),
skills=[
dict(name=skill, value=None, type="soft")
for skill in flatchr_job.get("skills", "").split(";")
for skill in (flatchr_job.get("skills") or "").split(";")
],
tags=get_tags(flatchr_job),
)
return hrflow_job


def format_flatchr_job_for_archive(flatchr_job: t.Dict) -> t.Dict:
return {"reference": flatchr_job["vacancy_id"]}
return {"reference": flatchr_job["id"]}


DESCRIPTION = (
Expand Down Expand Up @@ -167,13 +188,13 @@ def format_flatchr_job_for_archive(flatchr_job: t.Dict) -> t.Dict:
Mode.update,
Entity.profile,
Direction.outbound,
format=format_hrflow_profile,
format=format_hrflow_profile_for_update,
),
Flow(
Mode.archive,
Entity.profile,
Direction.outbound,
format=format_hrflow_for_archive,
format=format_hrflow_profile_for_archive,
),
Flow(
Mode.create,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Send **archived** 'job(s)' _from_ Flatchr _to_ HrFlow

| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| `api_key` :red_circle: | `string` | None | The API key to authenticate with the Flatchr API |
| `company_id` :red_circle: | `string` | None | The ID of the company to authenticate with |
| `env_base_url` | `Literal['https://api.demo.flatchr.io','https://api.flatchr.io/']` | https://api.flatchr.io/ | The base URL of the Flatchr API |

## HrFlow.ai Auth Parameters
Expand Down Expand Up @@ -56,6 +58,8 @@ Flatchr.archive_jobs_in_hrflow(
workflow_id=...,
logics=...,
connector_auth=dict(
api_key=...,
company_id=...,
env_base_url=...,
),
hrflow_auth=dict(
Expand Down
Loading

0 comments on commit e54f5b3

Please sign in to comment.