From eb4d294c2650d388584299a851e3bb99751324c9 Mon Sep 17 00:00:00 2001 From: Nedhir Date: Wed, 11 Dec 2024 18:04:29 +0100 Subject: [PATCH] feature: Add Breezy HR Connector in CRUD v2 --- src/hrflow_connectors/v2/__init__.py | 2 + .../v2/connectors/breezyhr/README.md | 71 ++ .../v2/connectors/breezyhr/__init__.py | 1 + .../v2/connectors/breezyhr/aisles.py | 636 ++++++++++++++++++ .../v2/connectors/breezyhr/connector.py | 327 +++++++++ .../connectors/breezyhr/docs/pull_job_list.md | 71 ++ .../breezyhr/docs/pull_profile_list.md | 71 ++ .../breezyhr/docs/push_profile_list.md | 73 ++ .../v2/connectors/breezyhr/logo.jpg | Bin 0 -> 6692 bytes .../format/archive_jobs_in_hrflow.json | 3 + .../format/archive_profiles_in_hrflow.json | 3 + .../format/create_jobs_in_hrflow.json | 90 +++ .../format/create_profiles_in_breezyhr.json | 11 + .../format/create_profiles_in_hrflow.json | 50 ++ .../format/update_jobs_in_hrflow.json | 90 +++ .../format/update_profiles_in_breezyhr.json | 12 + .../format/update_profiles_in_hrflow.json | 50 ++ .../v2/connectors/breezyhr/notebooks/.gitkeep | 0 .../v2/connectors/breezyhr/schemas.py | 112 +++ .../v2/connectors/breezyhr/test-config.yaml | 239 +++++++ .../v2/connectors/breezyhr/utils.py | 17 + .../v2/connectors/breezyhr/warehouse.py | 14 + 22 files changed, 1943 insertions(+) create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/README.md create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/__init__.py create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/aisles.py create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/connector.py create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_job_list.md create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_profile_list.md create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/docs/push_profile_list.md create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/logo.jpg create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_jobs_in_hrflow.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_profiles_in_hrflow.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_jobs_in_hrflow.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_breezyhr.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_hrflow.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_jobs_in_hrflow.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_breezyhr.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_hrflow.json create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/notebooks/.gitkeep create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/schemas.py create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/test-config.yaml create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/utils.py create mode 100644 src/hrflow_connectors/v2/connectors/breezyhr/warehouse.py diff --git a/src/hrflow_connectors/v2/__init__.py b/src/hrflow_connectors/v2/__init__.py index 5b02fcd28..d89fa8830 100644 --- a/src/hrflow_connectors/v2/__init__.py +++ b/src/hrflow_connectors/v2/__init__.py @@ -1,5 +1,6 @@ from hrflow_connectors.v2.connectors.admen import Admen from hrflow_connectors.v2.connectors.adzuna import Adzuna +from hrflow_connectors.v2.connectors.breezyhr import BreezyHR from hrflow_connectors.v2.connectors.bullhorn import Bullhorn from hrflow_connectors.v2.connectors.flatchr import Flatchr from hrflow_connectors.v2.connectors.francetravail import FranceTravail @@ -24,4 +25,5 @@ Adzuna, FranceTravail, Flatchr, + BreezyHR, ] diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/README.md b/src/hrflow_connectors/v2/connectors/breezyhr/README.md new file mode 100644 index 000000000..42ac58c8a --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/README.md @@ -0,0 +1,71 @@ +# πŸ“– Summary +- [πŸ“– Summary](#-summary) +- [πŸ“ About BreezyHR](#-about-breezyhr) +- [πŸ“Š Data Flow](#-data-flow) +- [πŸ”Œ Connector Actions](#-connector-actions) +- [🐍 Quick Start Examples](#-quick-start-examples) + - [**Push Profiles Action**](#push-profiles-action) + - [**Pull Jobs Action**](#pull-jobs-action) +- [πŸ”— Useful Links](#-useful-links) +- [πŸ‘ Special Thanks](#-special-thanks) + + +# πŸ“ About BreezyHR + +BreezyHr is a software designed to assist teams in finding the right candidates, evaluating applicants, and making a hire more quickly. +It helps streamline the recruitment process and identify the best fit for a position. + + +

+ +

+ +# πŸ“Š Data Flow +In this section, we outline the data flow between different components of the connector. The following schema provides a graphical representation of the data exchange process + +

+ +

+ +# πŸ”Œ Connector Actions +

+ +| Action | Description | +| ------- | ----------- | +| [**Pull job list**](docs/pull_job_list.md) | Retrieves all jobs via the ***BreezyHR*** API and send them to a ***Hrflow.ai Board***. | +| [**Push profile list**](docs/push_profile_list.md) | Push all profiles from ***Hrflow.ai Source*** via ***BreezyHR*** API and send them to a ***BreezyHR***. | +| [**Pull profile list**](docs/pull_profile_list.md) | Retrieves all profiles via the ***BreezyHR*** API and send them to a ***Hrflow.ai Source***. | + + +

+ + +# 🐍 Quick Start Examples + + +To make sure you can successfully run the latest versions of the example scripts, you have to **install the package from PyPi**. +To browse the examples of actions corresponding to released versions of πŸ€— this connector, you just need to import the module like this : + + +

+ +

+ +Once the connector module is imported, you can leverage all the different actions that it offers. + +For more code details checkout connector code + +# πŸ”— Useful Links + +- πŸ“„Visit [BreezyHR](https://breezy.hr/) to learn more. +- βš™οΈ API documentation : (https://developer.breezy.hr/reference/overview) +- πŸ’» [Connector code](https://github.com/Riminder/hrflow-connectors/tree/master/src/hrflow_connectors/connectors/breezyhr) on our Github. + + +# πŸ‘ Special Thanks +- πŸ’» HrFlow.ai : [Limam VADHEL](https://github.com/limamvadhel) - Software Engineer +- πŸ’» HrFlow.ai : [Leo FERRETTI](https://github.com/Sprenger07) - Software Engineer +- πŸ’» HrFlow.ai :[Corentin DUCHENE](https://github.com/CorentinDuchene) - Software Engineer +- πŸ’» HrFlow.ai : [Nedhir Ebnou](https://github.com/nedhirouebnou) - Software Engineer +- 🀝 BreezyHR :[Breezy HR for the partnership and accessible documentation](https://breezy.hr/) + diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/__init__.py b/src/hrflow_connectors/v2/connectors/breezyhr/__init__.py new file mode 100644 index 000000000..1528fac9d --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/__init__.py @@ -0,0 +1 @@ +from hrflow_connectors.v2.connectors.breezyhr.connector import BreezyHR # noqa diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/aisles.py b/src/hrflow_connectors/v2/connectors/breezyhr/aisles.py new file mode 100644 index 000000000..ef8e66a7c --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/aisles.py @@ -0,0 +1,636 @@ +import json +import typing as t +from datetime import datetime, timedelta +from enum import Enum +from io import BytesIO +from logging import LoggerAdapter + +import requests +from msgspec import Meta, Struct +from typing_extensions import Annotated + +from hrflow_connectors.v2.connectors.breezyhr.schemas import ( + BreezyJobModel, + BreezyProfileModel, +) +from hrflow_connectors.v2.core.common import Entity, Mode +from hrflow_connectors.v2.core.warehouse import ( + Aisle, + Criterias, + ReadOperation, + WriteOperation, + merge, +) + +BREEZY_BASE_URL = "https://api.breezy.hr/v3" + + +class State(str, Enum): + draft = "draft" + archived = "archived" + published = "published" + closed = "closed" + pending = "pending" + + +class Origin(str, Enum): + sourced = "sourced" + applied = "applied" + + +class AuthParameters(Struct): + email: Annotated[ + str, + Meta( + description="email", + ), + ] + password: Annotated[str, Meta(description="password")] + company_id: Annotated[ + t.Optional[str], + Meta( + description=( + "ID of company to pull jobs from in Breezy HR database associated with" + " the authenticated user" + ), + ), + ] = None + company_name: Annotated[ + t.Optional[str], + Meta( + description=( + "[⚠️ Requiered if company_id is not specified], the company associated" + " with the authenticated user" + ), + ), + ] = None + + +class ReadJobsParameters(Struct): + state: Annotated[ + State, + Meta( + description=( + "Specify an optional position state filter. e.g. draft, archived," + " published, closed, pending\nDefaults to published" + ), + ), + ] = State.published + + +class WriteProfilesParameters(Struct): + position_id: Annotated[ + str, + Meta( + description="Id of the position to create a new candidate for", + ), + ] + origin: Annotated[ + Origin, + Meta( + description=( + "will indicate in Breezy if the candidate should be marked as sourced" + " or applied" + ), + ), + ] = Origin.sourced + stage_actions_enabled: Annotated[ + bool, + Meta( + description=( + 'When origin is "sourced", should stage actions be executed (defaults' + ' to false). This is always true when origin is "applied"' + ), + ), + ] = False + + +class UpdateProfilesParameters(Struct): + position_id: Annotated[ + str, + Meta( + description="Id of the position to create a new candidate for", + ), + ] + + +class ReadProfilesParameters(Struct): + position_id: Annotated[ + t.Optional[str], + Meta( + description="Id of the position to create a new candidate for", + ), + ] = None + + +def get_access_token( + adapter: LoggerAdapter, + auth_parameters: AuthParameters, +): + sign_in_url = f"{BREEZY_BASE_URL}/signin" + + response = requests.post( + sign_in_url, + json={"email": auth_parameters.email, "password": auth_parameters.password}, + ) + + if not response.ok: + adapter.error(f"failed to retrieve access token, reason: {response.text}") + raise Exception("failed to retrieve access token") + + return response.json().get("access_token") + + +def revoke_access_token(access_token): + sign_out_url = f"{BREEZY_BASE_URL}/signout" + + headers = {"Authorization": f"{access_token}"} + + requests.get(sign_out_url, headers=headers) + + +def generic_jobs_read( + mode: Mode, +): + def read_jobs( + adapter: LoggerAdapter, + auth_parameters: AuthParameters, + parameters: ReadJobsParameters, + incremental: bool, + incremental_token: t.Optional[str], + ) -> t.Iterable[t.Dict]: + access_token = get_access_token(adapter, auth_parameters) + headers = {"Authorization": access_token} + + company_id = auth_parameters.company_id + if company_id is None: + companies_url = f"{BREEZY_BASE_URL}/companies" + + companies_response = requests.get(companies_url, headers=headers) + + if not companies_response.ok: + adapter.error( + f"Failed to retrieve company id, reason: {companies_response.text}" + ) + raise Exception("Failed to retrieve company id") + + company_list = companies_response.json() + for company in company_list: + if company["name"] == auth_parameters.company_name: + company_id = company["_id"] + break + if company_id is None: + adapter.error( + "Failed to retrieve company id, reason: company does not match with" + " an id" + ) + raise Exception("Failed to retrieve company id") + + url = f"{BREEZY_BASE_URL}/company/{company_id}/positions" + params = {"state": parameters.state} + + response = requests.get(url, headers=headers, params=params) + if not response.ok: + adapter.error(f"Failed to read jobs, reason: {response.text}") + raise Exception("Failed to read jobs") + + for job in response.json(): + if mode == Mode.create and not is_within_five_minutes( + job["creation_date"], job["updated_date"] + ): + continue + if mode == Mode.update and is_within_five_minutes( + job["creation_date"], job["updated_date"] + ): + continue + yield job + + revoke_access_token(access_token) + + return read_jobs + + +def send_profile( + adapter: LoggerAdapter, + parameters: WriteProfilesParameters, + profiles: t.Iterable[t.Dict], + access_token: str, + company_id: str, + candidate_id: str = "", +): + base_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/" + f"position/{parameters.position_id}/candidate" + ) + + payload = json.dumps(profiles) + + headers = {"Content-Type": "application/json", "Authorization": f"{access_token}"} + + if candidate_id == "": + url = f"{base_url}s/" + + # If the candidate doesn't already exist we "POST" his profile + response = requests.post(url, headers=headers, data=payload) + + else: + # In case the candidate exists, + # we retrieve his id to update his profile with a "PUT" request + + url = f"{base_url}/{candidate_id}" + + url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/" + f"position/{parameters.position_id}/candidate/{candidate_id}" + ) + + adapter.info(f"Updating id = {candidate_id} profile") + response = requests.put(url, headers=headers, data=payload) + return response + + +def write_profiles( + adapter: LoggerAdapter, + auth_parameters: AuthParameters, + parameters: WriteProfilesParameters, + items: t.Iterable[t.Dict], +) -> t.List[t.Dict]: + failed_profiles = [] + access_token = get_access_token(adapter, auth_parameters) + headers = {"Authorization": access_token} + + company_id = auth_parameters.company_id + + if company_id is None: + companies_url = f"{BREEZY_BASE_URL}/companies" + + companies_response = requests.get(companies_url, headers=headers) + + if not companies_response.ok: + adapter.error( + f"Failed to retrieve company id, reason: {companies_response.text}" + ) + raise Exception("Failed to retrieve company id") + + company_list = companies_response.json() + for company in company_list: + if company["name"] == auth_parameters.company_name: + company_id = company["_id"] + break + if company_id is None: + adapter.error( + "Failed to retrieve company id, reason: company does not match with" + " an id" + ) + raise Exception("Failed to retrieve company id") + + post_candidate_url = f"{BREEZY_BASE_URL}/company/{company_id}/position/{parameters.position_id}/candidates" + for profile in items: + resume_url = profile.pop("resume", None) + profile.update({"origin": parameters.origin}) + if parameters.stage_actions_enabled: + params = {"stage_actions_enabled": parameters.stage_actions_enabled} + else: + params = {} + + post_candidate_response = requests.post( + post_candidate_url, headers=headers, json=profile, params=params + ) + if not post_candidate_response.ok: + adapter.error( + f"Couldn't create candidate, reason: {post_candidate_response.text}" + ) + failed_profiles.append(profile) + continue + + if resume_url: + candidate_id = post_candidate_response.json()["_id"] + resume_file_response = requests.get(url=resume_url) + resume_file_obj = BytesIO(resume_file_response.content) + files = { + "file": ( + "resume.pdf", + resume_file_obj, + "application/pdf", + ) + } + post_resume_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/position/" + f"{parameters.position_id}/candidate/{candidate_id}/resume" + ) + post_resume_response = requests.post( + post_resume_url, headers=headers, files=files + ) + if not post_resume_response.ok: + adapter.error( + "Failed to attach resume to candidate, reason:" + f" {post_resume_response.text}" + ) + + revoke_access_token(access_token) + return failed_profiles + + +def update_profiles( + adapter: LoggerAdapter, + auth_parameters: AuthParameters, + parameters: UpdateProfilesParameters, + items: t.Iterable[t.Dict], +) -> t.List[t.Dict]: + failed_profiles = [] + access_token = get_access_token(adapter, auth_parameters) + headers = {"Authorization": access_token} + + company_id = auth_parameters.company_id + if company_id is None: + companies_url = f"{BREEZY_BASE_URL}/companies" + + companies_response = requests.get(companies_url, headers=headers) + + if not companies_response.ok: + adapter.error( + f"Failed to retrieve company id, reason: {companies_response.text}" + ) + raise Exception("Failed to retrieve company id") + + company_list = companies_response.json() + for company in company_list: + if company["name"] == auth_parameters.company_name: + company_id = company["_id"] + break + if company_id is None: + adapter.error( + "Failed to retrieve company id, reason: company does not match with" + " an id" + ) + raise Exception("Failed to retrieve company id") + + base_url = f"{BREEZY_BASE_URL}/company/{company_id}/position/{parameters.position_id}/candidate" + for profile in items: + candidate_id = profile.pop("id") + candidate_work_history = profile.pop("work_history", []) + candidate_education = profile.pop("education", []) + put_candidate_url = f"{base_url}/{candidate_id}" + put_candidate_response = requests.put( + put_candidate_url, headers=headers, json=profile + ) + if not put_candidate_response.ok: + adapter.error( + f"Couldn't create candidate, reason: {put_candidate_response.text}" + ) + failed_profiles.append(profile) + continue + + current_educations = [ + {education["school_name"], education["field_of_study"]} + for education in put_candidate_response.json().get("education", []) + ] + current_work_history = [ + {work_history["company_name"], work_history["title"]} + for work_history in put_candidate_response.json().get("work_history", []) + ] + + for education in candidate_education: + if { + education["school_name"], + education["field_of_study"], + } in current_educations: + continue + education_url = f"{put_candidate_url}/education" + education_response = requests.put( + education_url, headers=headers, json=education + ) + if not education_response.ok: + adapter.error( + f"Couldn't update education, for candidate {candidate_id}, reason:" + f" {education_response.text}" + ) + for work_history in candidate_work_history: + if { + work_history["company_name"], + work_history["title"], + } in current_work_history: + continue + work_history_url = f"{put_candidate_url}/work-history" + work_history_response = requests.put( + work_history_url, headers=headers, json=work_history + ) + if not work_history_response.ok: + adapter.error( + f"Couldn't update experience, for candidate {candidate_id}, reason:" + f" {work_history_response.text}" + ) + + revoke_access_token(access_token) + return failed_profiles + + +def generic_profiles_read( + mode: Mode, +): + def read_profiles( + adapter: LoggerAdapter, + auth_parameters: AuthParameters, + parameters: ReadProfilesParameters, + incremental: bool, + incremental_token: t.Optional[str], + ) -> t.Iterable[t.Dict]: + # TODO: add incremental read_mode using page_size, and page as read_from + access_token = get_access_token(adapter, auth_parameters) + headers = {"Authorization": access_token} + + company_id = auth_parameters.company_id + if company_id is None: + companies_url = f"{BREEZY_BASE_URL}/companies" + + companies_response = requests.get(companies_url, headers=headers) + + if not companies_response.ok: + adapter.error( + f"Failed to retrieve company id, reason: {companies_response.text}" + ) + raise Exception("Failed to retrieve company id") + + company_list = companies_response.json() + for company in company_list: + if company["name"] == auth_parameters.company_name: + company_id = company["_id"] + break + if company_id is None: + adapter.error( + "Failed to retrieve company id, reason: company does not match with" + " an id" + ) + raise Exception("Failed to retrieve company id") + if not parameters.position_id: + # retrieve all postion ids for all published positions + positions_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/positions?state=published" + ) + + positions_response = requests.get(positions_url, headers=headers) + if len(positions_response.json()) == 0: + adapter.info("No published position found") + for position in positions_response.json(): + position_id = position["_id"] + candidates_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/position/" + f"{position_id}/candidates?sort=updated" + ) + candidates_response = requests.get(candidates_url, headers=headers) + if not candidates_response.ok: + adapter.error( + "Failed to retrieve candidates, reason:" + f" {candidates_response.text}" + ) + raise Exception("Failed to retrieve candidates") + candidates = candidates_response.json() + for candidate in candidates: + if mode == Mode.create and not is_within_five_minutes( + candidate["creation_date"], candidate["updated_date"] + ): + continue + if mode == Mode.update and is_within_five_minutes( + candidate["creation_date"], candidate["updated_date"] + ): + continue + candidate_id = candidate["_id"] + full_candidate_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/position/" + f"{position_id}/candidate/{candidate_id}" + ) + full_candidate_response = requests.get( + full_candidate_url, headers=headers + ) + if not full_candidate_response.ok: + adapter.error( + "Failed to retrieve candidate, reason:" + f" {full_candidate_response.text}" + ) + continue + + full_candidate = full_candidate_response.json() + + resume_url = full_candidate.get("resume", {}).get("url") + if not resume_url: + full_candidate["resume"] = None + if resume_url: + resume_response = requests.get(resume_url, headers=headers) + if not resume_response.ok: + adapter.error( + "Failed to retrieve resume, reason:" + f" {resume_response.text}" + ) + full_candidate["resume"] = None + else: + full_candidate["resume"] = dict(raw=resume_response.content) + + yield full_candidate + else: + candidates_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/position/" + f"{parameters.position_id}/candidates?sort=updated" + ) + + candidates_response = requests.get(candidates_url, headers=headers) + if not candidates_response.ok: + adapter.error( + f"Failed to retrieve candidates, reason: {candidates_response.text}" + ) + raise Exception("Failed to retrieve candidates") + candidates = candidates_response.json() + for candidate in candidates: + if mode == Mode.create and not is_within_five_minutes( + candidate["creation_date"], candidate["updated_date"] + ): + continue + if mode == Mode.update and is_within_five_minutes( + candidate["creation_date"], candidate["updated_date"] + ): + continue + candidate_id = candidate["_id"] + full_candidate_url = ( + f"{BREEZY_BASE_URL}/company/{company_id}/position/" + f"{parameters.position_id}/candidate/{candidate_id}" + ) + full_candidate_response = requests.get( + full_candidate_url, headers=headers + ) + if not full_candidate_response.ok: + adapter.error( + "Failed to retrieve candidate, reason:" + f" {full_candidate_response.text}" + ) + continue + + full_candidate = full_candidate_response.json() + + resume_url = full_candidate.get("resume", {}).get("url") + if not resume_url: + full_candidate["resume"] = None + if resume_url: + resume_response = requests.get(resume_url, headers=headers) + if not resume_response.ok: + adapter.error( + f"Failed to retrieve resume, reason: {resume_response.text}" + ) + full_candidate["resume"] = None + else: + full_candidate["resume"] = dict(raw=resume_response.content) + + yield full_candidate + revoke_access_token(access_token) + + return read_profiles + + +# To account for the time differnece between the start of object creation on the platform +# and its completion +def is_within_five_minutes(date_str1: str, date_str2: str) -> bool: + date1 = datetime.fromisoformat(date_str1.replace("Z", "+00:00")) + date2 = datetime.fromisoformat(date_str2.replace("Z", "+00:00")) + + difference = abs(date1 - date2) + + return difference <= timedelta(minutes=5) + + +JobsAisle = Aisle( + name=Entity.job, + schema=BreezyJobModel, + read=ReadOperation( + criterias=Criterias( + create=ReadJobsParameters, + update=ReadJobsParameters, + archive=ReadJobsParameters, + ), + function=merge( + create=generic_jobs_read(Mode.create), + update=generic_jobs_read(Mode.update), + archive=generic_jobs_read(Mode.archive), + ), + ), +) + +ProfilesAisle = Aisle( + name=Entity.profile, + schema=BreezyProfileModel, + read=ReadOperation( + criterias=Criterias( + create=ReadProfilesParameters, + update=ReadProfilesParameters, + archive=ReadProfilesParameters, + ), + function=merge( + create=generic_profiles_read(Mode.create), + update=generic_profiles_read(Mode.update), + archive=generic_profiles_read(Mode.archive), + ), + ), + write=WriteOperation( + criterias=Criterias( + create=WriteProfilesParameters, update=UpdateProfilesParameters + ), + function=merge(create=write_profiles, update=update_profiles), + ), +) diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/connector.py b/src/hrflow_connectors/v2/connectors/breezyhr/connector.py new file mode 100644 index 000000000..6155281e5 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/connector.py @@ -0,0 +1,327 @@ +import typing as t + +from hrflow_connectors.v2.connectors.breezyhr.utils import ( + is_valid_url, + remove_html_tags, +) +from hrflow_connectors.v2.connectors.breezyhr.warehouse import BreezyHrWarehouse +from hrflow_connectors.v2.core.common import Direction, Entity, Mode +from hrflow_connectors.v2.core.connector import Connector, ConnectorType, Flow + + +def get_location(location: t.Dict) -> t.Dict: + country = location.get("country", {}).get("name") + city = location.get("city") + address = location.get("name") + street_address = location.get("streetAddress", {}).get("custom") + geojson = dict(country=country, city=city, text=street_address or address) + + return dict(text=street_address or address, geojson=geojson, lat=None, lng=None) + + +def format_job(breezy_job: t.Dict) -> t.Dict: + """ + Format a Breezy Hr job object into a hrflow job object + Returns: + HrflowJob: a job object in the hrflow job format + """ + location = get_location(breezy_job.get("location", {})) + is_remote = breezy_job.get("location", {}).get("is_remote") + remote_details = breezy_job.get("location", {}).get("remote_details") + description = remove_html_tags(breezy_job.get("description", "")) + cleaned_description = description.replace(" ", " ") + + sections = [ + dict( + name="description", + title="Description", + description=cleaned_description, + ), + dict( + name="experience", + title="Required Experience", + description=breezy_job.get("experience", {}).get("name"), + ), + dict( + name="education", + title="Required Education", + description=breezy_job.get("education"), + ), + dict( + name="category", + title="Category", + description=breezy_job.get("category", {}).get("name"), + ), + dict( + name="remote", + title="Remote Information", + description=remote_details, + ), + ] + t = lambda name, value: dict(name=name, value=value) + tags = [ + t("type", breezy_job.get("type", {}).get("name")), + t("experience", breezy_job.get("experience", {}).get("name")), + t("education", breezy_job.get("education")), + t("department", breezy_job.get("department")), + t("requisition_id", breezy_job.get("requisition_id")), + t("category", breezy_job.get("category", {}).get("name")), + t("candidate_type", breezy_job.get("candidate_type")), + t("isremote", is_remote), + t("remote_details", remote_details), + t("creator_id", breezy_job.get("creator_id")), + t("breezy_hr_tags", breezy_job.get("tags")), + ] + + hrflow_job = dict( + name=breezy_job.get("name"), + reference=breezy_job.get("_id"), + summary=cleaned_description, + location=location, + sections=sections, + tags=tags, + created_at=breezy_job.get("creation_date"), + updated_at=breezy_job.get("updated_date"), + ) + + return hrflow_job + + +def format_archive_in_hrflow(breezy_element: t.Dict) -> t.Dict: + return dict(reference=breezy_element.get("_id")) + + +def format_profile(hrflow_profile: t.Dict) -> t.Dict: + """ + Format a Hrflow profile object into a breezy hr profile object + Args: + data (HrflowProfile): Hrflow Profile to format + Returns: + BreezyProfileModel: a BreezyHr formatted profile object + """ + + info = hrflow_profile.get("info", {}) + + work_history = [] + for experience in hrflow_profile.get("experiences", []): + formatted_experience = dict() + if experience.get("company") not in ["", None]: + formatted_experience["company_name"] = experience.get("company") + else: + formatted_experience["company_name"] = "Undefined" + formatted_experience["title"] = experience.get("title") + formatted_experience["summary"] = experience.get("description") + if experience.get("date_start") is not None: + date_start = experience["date_start"] + formatted_experience["start_year"] = int(date_start[:4]) + formatted_experience["start_month"] = int(date_start[5:7]) + if experience.get("date_end") is not None: + date_end = experience["date_end"] + formatted_experience["end_year"] = int(date_end[:4]) + formatted_experience["end_month"] = int(date_end[5:7]) + work_history.append(formatted_experience) + + educations = [] + for education in hrflow_profile.get("educations", []): + formatted_education = dict() + if education.get("school") == "": + education["school"] = "Undefined" + formatted_education["school_name"] = education.get("school") + formatted_education["field_of_study"] = education.get("title") + if education.get("date_start") is not None: + date_start = education["date_start"] + formatted_education["start_year"] = int(date_start[:4]) + if education.get("date_end") is not None: + date_end = education["date_end"] + formatted_education["end_year"] = int(date_end[:4]) + educations.append(formatted_education) + + social_profiles = {} + if info.get("urls"): + for url in info.get("urls"): + type = url.get("type") + link = url.get("url") + if type and link: + if type == "from_resume" or not is_valid_url(link): + continue + social_profiles.update({type: link}) + + # add profile skills to tags + tags = [] + skills = hrflow_profile.get("skills") + if skills: + tags = [skill.get("name") for skill in skills] + + # add resume to profile + attachments = hrflow_profile.get("attachments", []) + resume_url = next( + ( + attachment + for attachment in attachments + if attachment.get("type") == "resume" + ), + {}, + ).get("public_url") + breezy_profile = dict( + name=info.get("full_name"), + email_address=info.get("email"), + phone_number=info.get("phone"), + address=info.get("location", {}).get("text"), + summary=info.get("summary"), + work_history=work_history, + education=educations, + social_profiles=social_profiles, + tags=tags, + resume=resume_url, + ) + return breezy_profile + + +def format_profile_for_update(hrflow_profile: t.Dict) -> t.Dict: + breezy_profile = format_profile(hrflow_profile) + breezy_profile["id"] = hrflow_profile.get("reference") + return breezy_profile + + +def format_date_to_iso(date): + year = date.get("year") + month = date.get("month") + day = date.get("day") + # Check if the date is complete, i.e., year, month, and day are all present + if year is not None and month is not None and day is not None: + return f"{year:04d}-{month:02d}-{day:02d}" + elif year is not None and month is not None: + return f"{year:04d}-{month:02d}" + elif year is not None: + return f"{year:04d}" + else: + return None + + +def format_candidate(breezy_profile: t.Dict) -> t.Dict: + """ + Format a Breezy profile object into a Hrflow profile object + Args: + data (BreezyProfileModel): Breezy Profile to format + Returns: + HrFlowProfile: a Hrflow formatted profile object + """ + info = dict( + full_name=breezy_profile["name"], + first_name=breezy_profile["name"].split(" ")[0], + last_name=breezy_profile["name"].split(" ")[-1], + email=breezy_profile["email_address"], + phone=breezy_profile["phone_number"], + urls=[ + dict(type=social_profile["type"], url=social_profile["url"]) + for social_profile in breezy_profile.get("social_profiles", []) + ], + summary=breezy_profile["summary"], + location={"text": breezy_profile.get("address", ""), "lat": None, "lng": None}, + ) + + educations = [] + for education in breezy_profile.get("education", []): + formatted_education = dict() + formatted_education["school"] = education.get("school_name") + degree = education.get("degree") + field_of_study = education.get("field_of_study") + if degree or field_of_study: + formatted_education["title"] = ( + (degree if degree else "") + + " " + + (field_of_study if field_of_study else "") + ).strip() + formatted_education["description"] = ( + formatted_education["title"] + " at " + formatted_education["school"] + ) + formatted_education["date_start"] = None + formatted_education["date_end"] = None + if education.get("start_date") is not None: + formatted_education["date_start"] = format_date_to_iso( + education["start_date"] + ) + if education.get("end_date") is not None: + formatted_education["date_end"] = format_date_to_iso(education["end_date"]) + formatted_education["location"] = {"text": None, "lat": None, "lng": None} + educations.append(formatted_education) + + experiences = [] + for experience in breezy_profile.get("work_history", []): + formatted_experience = dict() + formatted_experience["company"] = experience.get("company_name") + + formatted_experience["title"] = experience.get("title") + formatted_experience["description"] = experience.get("summary") + formatted_experience["date_start"] = None + formatted_experience["date_end"] = None + if experience.get("start_date") is not None: + formatted_experience["date_start"] = format_date_to_iso( + experience["start_date"] + ) + if experience.get("end_date") is not None: + formatted_experience["date_end"] = format_date_to_iso( + experience["end_date"] + ) + formatted_experience["location"] = {"text": None, "lat": None, "lng": None} + experiences.append(formatted_experience) + + tags = [] + t = lambda name, value: dict(name=name, value=value) + tags.append(t("breezy_hr_tags", breezy_profile.get("tags"))) + tags.append(t("headline", breezy_profile.get("headline"))) + tags.append(t("origin", breezy_profile.get("origin"))) + tags.append(t("source", breezy_profile.get("source", {}).get("name"))) + tags.append(t("sourced_by", breezy_profile.get("sourced_by"))) + tags.append(t("stage", breezy_profile.get("stage", {}).get("name"))) + tags.append( + t("overall_score", breezy_profile.get("overall_score", {}).get("average_score")) + ) + hrflow_profile = dict( + reference=breezy_profile.get("_id"), + info=info, + created_at=breezy_profile.get("creation_date"), + updated_at=breezy_profile.get("updated_date"), + experiences=experiences, + educations=educations, + skills=[], + tags=tags, + resume=breezy_profile.get("resume"), + ) + + return hrflow_profile + + +BreezyHR = Connector( + name="Breezy HR", + type=ConnectorType.ATS, + subtype="breezyhr", + description=( + "Breezyhr is an end-to-end recruiting software " + "to help you attract & hire great employees with less effort" + ), + url="https://breezy.hr/", + warehouse=BreezyHrWarehouse, + flows=( + Flow(Mode.create, Entity.job, Direction.inbound, format=format_job), + Flow(Mode.update, Entity.job, Direction.inbound, format=format_job), + Flow( + Mode.archive, Entity.job, Direction.inbound, format=format_archive_in_hrflow + ), + Flow(Mode.create, Entity.profile, Direction.inbound, format=format_candidate), + Flow(Mode.update, Entity.profile, Direction.inbound, format=format_candidate), + Flow(Mode.create, Entity.profile, Direction.outbound, format=format_profile), + Flow( + Mode.update, + Entity.profile, + Direction.outbound, + format=format_profile_for_update, + ), + Flow( + Mode.archive, + Entity.profile, + Direction.inbound, + format=format_archive_in_hrflow, + ), + ), +) diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_job_list.md b/src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_job_list.md new file mode 100644 index 000000000..fcde3590f --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_job_list.md @@ -0,0 +1,71 @@ +# Pull job list +`BreezyHRJobWarehouse` :arrow_right: `HrFlow.ai Jobs` + +Retrieves all jobs via the ***BreezyHR*** API and send them to a ***Hrflow.ai Board***. + + + +## Action Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `logics` | `typing.List[typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]]` | [] | List of logic functions | +| `format` | `typing.Callable[[typing.Dict], typing.Dict]` | [`format_jobs`](../connector.py#L31) | Formatting function | +| `read_mode` | `str` | ReadMode.sync | If 'incremental' then `read_from` of the last run is given to Origin Warehouse during read. **The actual behavior depends on implementation of read**. In 'sync' mode `read_from` is neither fetched nor given to Origin Warehouse during read. | + +## Source Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `email` :red_circle: | `str` | None | email | +| `password` :red_circle: | `str` | None | password | +| `company_id` | `str` | None | ID of company to pull jobs from in Breezy HR database associated with the authenticated user | +| `company_name` | `str` | None | [⚠️ Requiered if company_id is not specified], the company associated with the authenticated user | + +## Destination Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `api_secret` :red_circle: | `str` | None | X-API-KEY used to access HrFlow.ai API | +| `api_user` :red_circle: | `str` | None | X-USER-EMAIL used to access HrFlow.ai API | +| `board_key` :red_circle: | `str` | None | HrFlow.ai board key | +| `sync` | `bool` | True | When enabled only pushed jobs will remain in the board | +| `update_content` | `bool` | False | When enabled jobs already present in the board are updated | +| `enrich_with_parsing` | `bool` | False | When enabled jobs are enriched with HrFlow.ai parsing | + +:red_circle: : *required* + +## Example + +```python +import logging +from hrflow_connectors import BreezyHR +from hrflow_connectors.core import ReadMode + + +logging.basicConfig(level=logging.INFO) + + +BreezyHR.pull_job_list( + workflow_id="some_string_identifier", + action_parameters=dict( + logics=[], + format=lambda *args, **kwargs: None # Put your code logic here, + read_mode=ReadMode.sync, + ), + origin_parameters=dict( + email="your_email", + password="your_password", + company_id="your_company_id", + company_name="your_company_name", + ), + target_parameters=dict( + api_secret="your_api_secret", + api_user="your_api_user", + board_key="your_board_key", + sync=True, + update_content=False, + enrich_with_parsing=False, + ) +) +``` \ No newline at end of file diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_profile_list.md b/src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_profile_list.md new file mode 100644 index 000000000..738b15af6 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/docs/pull_profile_list.md @@ -0,0 +1,71 @@ +# Pull profile list +`BreezyHRWarehouse` :arrow_right: `HrFlow.ai Profiles` + +Retrieves all profiles via the ***BreezyHR*** API and send them to a ***Hrflow.ai Source***. + + + +## Action Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `logics` | `typing.List[typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]]` | [] | List of logic functions | +| `format` | `typing.Callable[[typing.Dict], typing.Dict]` | [`format_candidate`](../connector.py#L208) | Formatting function | +| `read_mode` | `str` | ReadMode.sync | If 'incremental' then `read_from` of the last run is given to Origin Warehouse during read. **The actual behavior depends on implementation of read**. In 'sync' mode `read_from` is neither fetched nor given to Origin Warehouse during read. | + +## Source Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `email` :red_circle: | `str` | None | email | +| `password` :red_circle: | `str` | None | password | +| `company_id` | `str` | None | ID of company to pull jobs from in Breezy HR database associated with the authenticated user | +| `company_name` | `str` | None | [⚠️ Requiered if company_id is not specified], the company associated with the authenticated user | +| `position_id` :red_circle: | `str` | None | Id of the position to create a new candidate for | + +## Destination Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `api_secret` :red_circle: | `str` | None | X-API-KEY used to access HrFlow.ai API | +| `api_user` :red_circle: | `str` | None | X-USER-EMAIL used to access HrFlow.ai API | +| `source_key` :red_circle: | `str` | None | HrFlow.ai source key | +| `edit` | `bool` | False | When enabled the profile must exist in the source | +| `only_edit_fields` :red_circle: | `typing.List[str]` | None | List of attributes to use for the edit operation e.g. ['tags', 'metadatas'] | + +:red_circle: : *required* + +## Example + +```python +import logging +from hrflow_connectors import BreezyHR +from hrflow_connectors.core import ReadMode + + +logging.basicConfig(level=logging.INFO) + + +BreezyHR.pull_profile_list( + workflow_id="some_string_identifier", + action_parameters=dict( + logics=[], + format=lambda *args, **kwargs: None # Put your code logic here, + read_mode=ReadMode.sync, + ), + origin_parameters=dict( + email="your_email", + password="your_password", + company_id="your_company_id", + company_name="your_company_name", + position_id="your_position_id", + ), + target_parameters=dict( + api_secret="your_api_secret", + api_user="your_api_user", + source_key="your_source_key", + edit=False, + only_edit_fields=***, + ) +) +``` \ No newline at end of file diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/docs/push_profile_list.md b/src/hrflow_connectors/v2/connectors/breezyhr/docs/push_profile_list.md new file mode 100644 index 000000000..355e85cb7 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/docs/push_profile_list.md @@ -0,0 +1,73 @@ +# Push profile list +`HrFlow.ai Profiles` :arrow_right: `BreezyHRWarehouse` + +Push all profiles from ***Hrflow.ai Source*** via ***BreezyHR*** API and send them to a ***BreezyHR***. + + + +## Action Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `logics` | `typing.List[typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]]` | [] | List of logic functions | +| `format` | `typing.Callable[[typing.Dict], typing.Dict]` | [`format_profile`](../connector.py#L94) | Formatting function | +| `read_mode` | `str` | ReadMode.sync | If 'incremental' then `read_from` of the last run is given to Origin Warehouse during read. **The actual behavior depends on implementation of read**. In 'sync' mode `read_from` is neither fetched nor given to Origin Warehouse during read. | + +## Source Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `api_secret` :red_circle: | `str` | None | X-API-KEY used to access HrFlow.ai API | +| `api_user` :red_circle: | `str` | None | X-USER-EMAIL used to access HrFlow.ai API | +| `source_key` :red_circle: | `str` | None | HrFlow.ai source key | +| `profile_key` :red_circle: | `str` | None | HrFlow.ai profile key | + +## Destination Parameters + +| Field | Type | Default | Description | +| ----- | ---- | ------- | ----------- | +| `email` :red_circle: | `str` | None | email | +| `password` :red_circle: | `str` | None | password | +| `company_id` | `str` | None | ID of company to pull jobs from in Breezy HR database associated with the authenticated user + [⚠️ Requiered if company_name is not specified] | +| `company_name` | `str` | None | the company associated with the authenticated user + [⚠️ Requiered if company_id is not specified] | +| `position_id` :red_circle: | `str` | None | Id of the position to create a new candidate for | +| `origin` | `str` | sourced | will indicate in Breezy if the candidate should be marked as sourced or applied | + +:red_circle: : *required* + +## Example + +```python +import logging +from hrflow_connectors import BreezyHR +from hrflow_connectors.core import ReadMode + + +logging.basicConfig(level=logging.INFO) + + +BreezyHR.push_profile_list( + workflow_id="some_string_identifier", + action_parameters=dict( + logics=[], + format=lambda *args, **kwargs: None # Put your code logic here, + read_mode=ReadMode.sync, + ), + origin_parameters=dict( + api_secret="your_api_secret", + api_user="your_api_user", + source_key="your_source_key", + profile_key="your_profile_key", + ), + target_parameters=dict( + email="your_email", + password="your_password", + company_id="your_company_id", + company_name="your_company_name", + position_id="your_position_id", + origin="sourced", + ) +) +``` \ No newline at end of file diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/logo.jpg b/src/hrflow_connectors/v2/connectors/breezyhr/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0eed272e677a922091515739477db28f3f13ef7 GIT binary patch literal 6692 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 literal 0 HcmV?d00001 diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_jobs_in_hrflow.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_jobs_in_hrflow.json new file mode 100644 index 000000000..018c86b68 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_jobs_in_hrflow.json @@ -0,0 +1,3 @@ +{ + "reference": "?._id" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_profiles_in_hrflow.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_profiles_in_hrflow.json new file mode 100644 index 000000000..018c86b68 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/archive_profiles_in_hrflow.json @@ -0,0 +1,3 @@ +{ + "reference": "?._id" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_jobs_in_hrflow.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_jobs_in_hrflow.json new file mode 100644 index 000000000..4f08dba1b --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_jobs_in_hrflow.json @@ -0,0 +1,90 @@ +{ + "name": "?.name", + "reference": "?._id", + "summary": "?.description | $sub('<[^<]+?>', '') | $sub(' ', ' ')", + "location": { + "text": "?.location?.streetAddress?.custom ?? .location.streetAddress.custom: .location.name", + "geojson": { + "country": "?.location?.country?.name", + "city": "?.location?.city", + "text": "?.location?.streetAddress?.custom ?? .location.streetAddress.custom: .location.name" + }, + "lat": null, + "lng": null + }, + "sections": [ + { + "name": "description", + "title": "description", + "description": "?.description | $sub('<[^<]+?>', '') | $sub(' ', ' ')" + }, + { + "name": "experience", + "title": "Required Experience", + "description": "?.experience?.name" + }, + { + "name": "education", + "title": "Required Education", + "description": "?.education?.name" + }, + { + "name": "category", + "title": "Category", + "description": "?.category?.name" + }, + { + "name": "remote", + "title": "Remote Information", + "description": "?.location?.remote_details" + } + ], + "tags": [ + { + "name": "type", + "value": "?.type?.name" + }, + { + "name": "experience", + "value": "?.experience?.name" + }, + { + "name": "education", + "value": "?.education" + }, + { + "name": "department", + "value": "?.department" + }, + { + "name": "requisition_id", + "value": "?.requisition_id" + }, + { + "name": "category", + "value": "?.category?.name" + }, + { + "name": "candidate_type", + "value": "?.candidate_type" + }, + { + "name": "isremote", + "value": "?.location?.is_remote" + }, + { + "name": "remote_details", + "value": "?.location?.remote_details" + }, + { + "name": "creator_id", + "value": "?.creator_id" + }, + { + "name": "breezy_hr_tags", + "value": "?.tags" + } + ], + "created_at": "?.creation_date", + "updated_at": "?.updated_date" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_breezyhr.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_breezyhr.json new file mode 100644 index 000000000..1296845b1 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_breezyhr.json @@ -0,0 +1,11 @@ +{ + "name": "?.info?.full_name", + "email_address": "?.info?.email", + "phone_number": "?.info?.phone", + "address": "?.info?.location?.text", + "summary": "?.info?.summary", + "work_history": "?.experiences | $map({company_name: ?.company >> 'Undefined', title: ?.title, summary: ?.description, start_year: ?.date_start != null ?? .date_start | $slice(0, 3) | $int, start_month: ?.date_start != null ?? .date_start | $slice(4,7) | $int, end_year: ?.date_end != null ?? .date_end | $slice(0, 3) | $int, end_month: ?.date_end != null ?? .date_end | $slice(4,7) | $int})", + "education": "?.educations | $map({school_name: ?.school >> 'Undefined', field_of_study: ?.title, start_year: ?.date_start != null ?? .date_start | $slice(0, 3) | $int, end_year: ?.date_end != null ?? .date_end | $slice(0, 3) | $int})", + "social_profiles": ".info?.urls >> [] | $map({.type: .url | $sub('(^https?://)(.*)', 'https://$2')})", + "tags": "?.skills >> [] | $map(?.name)" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_hrflow.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_hrflow.json new file mode 100644 index 000000000..48f490e80 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/create_profiles_in_hrflow.json @@ -0,0 +1,50 @@ +{ + "reference": "?._id", + "info": { + "full_name": ".name", + "first_name": ".name | $split(' ') | .[0]", + "last_name": ".name | $split(' ') | .[-1]", + "email": ".email_address", + "phone": ".phone_number", + "urls": "?.social_profiles || [] | $map({type: .type, url: .url})", + "summary": ".summary", + "location": { + "text": "?.address", + "lat": null, + "lng": null + } + }, + "created_at": "?.creation_date", + "updated_at": "?.updated_date", + "experiences": "?.work_history || [] | $map({company: ?.company_name, title: ?.title, description: ?.summary, date_start: ?.start_date.year != null and ?.start_date.month != null and ?.start_date.day != null ?? .start_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, date_end: ?.end_date.year != null and ?.end_date.month != null and ?.end_date.day != null ?? .end_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, location: {text: null, lat: null, lng: null}})", + "educations": "?.education || [] | $map({school: ?.school_name, title: $concat(?.degree || '', ' ', ?.field_of_study || '')| $strip, description: $concat(?.degree || '', ' ', ?.field_of_study || '', ' at ', ?.school_name || '')| $strip, date_start: ?.start_date.year != null and ?.start_date.month != null and ?.start_date.day != null ?? .start_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, date_end: ?.end_date.year != null and ?.end_date.month != null and ?.end_date.day != null ?? .end_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, location: {text: null, lat: null, lng: null}})", + "skills": [], + "tags": [ + { + "name": "breezy_hr_tags", + "value": "?.tags" + }, + { + "name": "headline", + "value": "?.headline" + }, + { + "name": "origin", + "value": "?.origin" + }, + { + "name": "source", + "value": "?.source?.name" + }, + { + "name": "sourced_by", + "value": "?.sourced_by" + }, + { "name": "stage", "value": "?.stage?.name" }, + { + "name": "overall_score", + "value": "?.overall_score?.average_score" + } + ], + "resume": "?.resume" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_jobs_in_hrflow.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_jobs_in_hrflow.json new file mode 100644 index 000000000..4f08dba1b --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_jobs_in_hrflow.json @@ -0,0 +1,90 @@ +{ + "name": "?.name", + "reference": "?._id", + "summary": "?.description | $sub('<[^<]+?>', '') | $sub(' ', ' ')", + "location": { + "text": "?.location?.streetAddress?.custom ?? .location.streetAddress.custom: .location.name", + "geojson": { + "country": "?.location?.country?.name", + "city": "?.location?.city", + "text": "?.location?.streetAddress?.custom ?? .location.streetAddress.custom: .location.name" + }, + "lat": null, + "lng": null + }, + "sections": [ + { + "name": "description", + "title": "description", + "description": "?.description | $sub('<[^<]+?>', '') | $sub(' ', ' ')" + }, + { + "name": "experience", + "title": "Required Experience", + "description": "?.experience?.name" + }, + { + "name": "education", + "title": "Required Education", + "description": "?.education?.name" + }, + { + "name": "category", + "title": "Category", + "description": "?.category?.name" + }, + { + "name": "remote", + "title": "Remote Information", + "description": "?.location?.remote_details" + } + ], + "tags": [ + { + "name": "type", + "value": "?.type?.name" + }, + { + "name": "experience", + "value": "?.experience?.name" + }, + { + "name": "education", + "value": "?.education" + }, + { + "name": "department", + "value": "?.department" + }, + { + "name": "requisition_id", + "value": "?.requisition_id" + }, + { + "name": "category", + "value": "?.category?.name" + }, + { + "name": "candidate_type", + "value": "?.candidate_type" + }, + { + "name": "isremote", + "value": "?.location?.is_remote" + }, + { + "name": "remote_details", + "value": "?.location?.remote_details" + }, + { + "name": "creator_id", + "value": "?.creator_id" + }, + { + "name": "breezy_hr_tags", + "value": "?.tags" + } + ], + "created_at": "?.creation_date", + "updated_at": "?.updated_date" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_breezyhr.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_breezyhr.json new file mode 100644 index 000000000..69fc2b7b1 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_breezyhr.json @@ -0,0 +1,12 @@ +{ + "id": "?.reference", + "name": "?.info?.full_name", + "email_address": "?.info?.email", + "phone_number": "?.info?.phone", + "address": "?.info?.location?.text", + "summary": "?.info?.summary", + "work_history": "?.experiences | $map({company_name: ?.company >> 'Undefined', title: ?.title, summary: ?.description, start_year: ?.date_start != null ?? .date_start | $slice(0, 3) | $int, start_month: ?.date_start != null ?? .date_start | $slice(4,7) | $int, end_year: ?.date_end != null ?? .date_end | $slice(0, 3) | $int, end_month: ?.date_end != null ?? .date_end | $slice(4,7) | $int})", + "education": "?.educations | $map({school_name: ?.school >> 'Undefined', field_of_study: ?.title, start_year: ?.date_start != null ?? .date_start | $slice(0, 3) | $int, end_year: ?.date_end != null ?? .date_end | $slice(0, 3) | $int})", + "social_profiles": ".info?.urls >> [] | $map({.type: .url | $sub('(^https?://)(.*)', 'https://$2')})", + "tags": "?.skills >> [] | $map(?.name)" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_hrflow.json b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_hrflow.json new file mode 100644 index 000000000..f2f9f56de --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/mappings/format/update_profiles_in_hrflow.json @@ -0,0 +1,50 @@ +{ + "reference": "?._id", + "info": { + "full_name": ".name", + "first_name": ".name | $split(' ') | .[0]", + "last_name": ".name | $split(' ') | .[-1]", + "email": ".email_address", + "phone": ".phone_number", + "urls": "?.social_profiles || [] | $map({type: .type, url: .url})", + "summary": ".summary", + "location": { + "text": ".address", + "lat": null, + "lng": null + } + }, + "created_at": "?.creation_date", + "updated_at": "?.updated_date", + "experiences": "?.work_history || [] | $map({company: ?.company_name, title: ?.title, description: ?.summary, date_start: ?.start_date.year != null and ?.start_date.month != null and ?.start_date.day != null ?? .start_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, date_end: ?.end_date.year != null and ?.end_date.month != null and ?.end_date.day != null ?? .end_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, location: {text: null, lat: null, lng: null}})", + "educations": "?.education || [] | $map({school: ?.school_name, title: $concat(?.degree || '', ' ', ?.field_of_study || '')| $strip, description: $concat(?.degree || '', ' ', ?.field_of_study || '', ' at ', ?.school_name || '')| $strip, date_start: ?.start_date.year != null and ?.start_date.month != null and ?.start_date.day != null ?? .start_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, date_end: ?.end_date.year != null and ?.end_date.month != null and ?.end_date.day != null ?? .end_date | $concat(.year, '-', $concat('0', .month) | $slice(-2), '-', $concat('0', .day) | $slice(-2)) : null, location: {text: null, lat: null, lng: null}})", + "skills": [], + "tags": [ + { + "name": "breezy_hr_tags", + "value": "?.tags" + }, + { + "name": "headline", + "value": "?.headline" + }, + { + "name": "origin", + "value": "?.origin" + }, + { + "name": "source", + "value": "?.source?.name" + }, + { + "name": "sourced_by", + "value": "?.sourced_by" + }, + { "name": "stage", "value": "?.stage?.name" }, + { + "name": "overall_score", + "value": "?.overall_score?.average_score" + } + ], + "resume": "?.resume" +} diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/notebooks/.gitkeep b/src/hrflow_connectors/v2/connectors/breezyhr/notebooks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/schemas.py b/src/hrflow_connectors/v2/connectors/breezyhr/schemas.py new file mode 100644 index 000000000..f56eea7b1 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/schemas.py @@ -0,0 +1,112 @@ +from typing import Any, Dict, List, Optional + +from msgspec import Meta, Struct +from typing_extensions import Annotated + + +class Type(Struct): + id: str + name: str + + +class Experience(Struct): + id: str + name: str + + +class Country(Struct): + name: str + id: str + + +class Location(Struct): + country: Country + city: str + is_remote: Optional[bool] + name: str + + +class Category(Struct): + id: str + name: str + + +class ApplicationForm(Struct): + name: str + headline: str + summary: str + profile_photo: str + address: str + email_address: str + phone_number: str + resume: str + work_history: str + education: str + cover_letter: str + questionnaire_in_experience: bool + + +class BreezyJobModel(Struct): + _id: Annotated[str, Meta(description="position id")] + type: Annotated[Type, Meta(description="job type")] + state: Annotated[ + str, + Meta( + description="state of the position posting, published or internal and so on" + ), + ] + name: Annotated[str, Meta(description="job title")] + friendly_id: Annotated[ + str, + Meta(description="another id of the job which combines its title and its id"), + ] + experience: Optional[Experience] + location: Location + education: str + department: str + description: Annotated[str, Meta(description="Job category")] + category: Category + application_form: Annotated[ + Optional[ApplicationForm], Meta(description="job Application for") + ] + creator_id: Optional[str] + creation_date: str + updated_date: str + all_users: List[str] + all_admins: List[str] + candidate_type: str + tags: List + org_type: str + + +class WorkHistoryItem(Struct): + company_name: str + title: str + summary: str + start_month: Optional[int] + start_year: Optional[int] + end_month: Optional[int] + end_year: Optional[int] + + +class EducationItem(Struct): + school_name: str + field_of_study: str + start_year: Optional[int] + end_year: Optional[int] + + +class BreezyProfileModel(Struct): + name: str + email_address: str + phone_number: str + summary: str + tags: Optional[List[str]] + source: Optional[str] + origin: Optional[str] + address: str + work_history: List[WorkHistoryItem] + education: List[EducationItem] + social_profiles: Optional[List[str]] + custom_attributes: Optional[List[Dict[str, Any]]] + cover_letter: Optional[str] diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/test-config.yaml b/src/hrflow_connectors/v2/connectors/breezyhr/test-config.yaml new file mode 100644 index 000000000..4fd42883e --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/test-config.yaml @@ -0,0 +1,239 @@ +warehouse: + BreezyHRJobWarehouse: + read: + - parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + + - parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + + - parameters: + email: $__EMAIL + password: $__PASSWORD + company_name : $__COMPANY_NAME + +actions: + pull_jobs: + - id: valid_parameters + origin_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: success + + - id: valid_parameters_without_company_name + origin_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: success + + - id: valid_parameters_without_company_id + origin_parameters: + email: $__EMAIL + password: $__PASSWORD + company_name : $__COMPANY_NAME + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: success + + - id: invalid_parameters_missing_company_id_or_name + origin_parameters: + email: $__EMAIL + password: $__PASSWORD + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + - id: invalid_parameters_email + origin_parameters: + email: invalid + password: $__PASSWORD + company_name : $__COMPANY_NAME + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + - id: missing_parameters_email + origin_parameters: + password: $__PASSWORD + company_name : $__COMPANY_NAME + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + - id: invalid_parameters_password + origin_parameters: + email: $__EMAIL + password: invalid + company_name : $__COMPANY_NAME + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + - id: missing_parameters_password + origin_parameters: + email: $__EMAIL + company_name : $__COMPANY_NAME + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + - id: invalid_parameters_company_id + origin_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id: invalid + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + - id: invalid_parameters_company_name + origin_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : invalid + target_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + board_key: $__BOARD_KEY + status: fatal + + push_profiles: + - id: valid_parameters_update_profile + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : $__POSITION_ID + status: success + + - id: valid_parameters_send_profile + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY_WITH_NO_EMAIL + target_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : $__POSITION_ID + status: success + + - id: invalid_parameters_mail + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + email: invalid + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : $__POSITION_ID + status: fatal + + - id: missing_parameters_mail + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : $__POSITION_ID + status: fatal + + - id: invalid_parameters_password + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + email: $__EMAIL + password: invalid + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : $__POSITION_ID + status: fatal + + + - id: missing_parameters_password + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + email: $__EMAIL + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : $__POSITION_ID + status: fatal + + - id: invalid_parameters_position_id + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + position_id : invalid + status: fatal + + - id: missing_parameters_position_id + origin_parameters: + api_secret: $__API_SECRET + api_user: $__API_USER + source_key: $__SOURCE_KEY + profile_key: $__PROFILE_KEY + target_parameters: + email: $__EMAIL + password: $__PASSWORD + company_id : $__COMPANY_ID + company_name : $__COMPANY_NAME + status: fatal \ No newline at end of file diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/utils.py b/src/hrflow_connectors/v2/connectors/breezyhr/utils.py new file mode 100644 index 000000000..b5c9b0fb0 --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/utils.py @@ -0,0 +1,17 @@ +import re + + +def remove_html_tags(text: str) -> str: + """ + Remove all HTML tags in a string + Args: + text (str): text to clean + Returns: + str: cleaned text (without HTML tags) + """ + return re.sub("<[^<]+?>", "", text) + + +def is_valid_url(url): + regex = r"^https?:\/\/(www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/?[^\s]*$" + return re.match(regex, url) is not None diff --git a/src/hrflow_connectors/v2/connectors/breezyhr/warehouse.py b/src/hrflow_connectors/v2/connectors/breezyhr/warehouse.py new file mode 100644 index 000000000..3c054ed7e --- /dev/null +++ b/src/hrflow_connectors/v2/connectors/breezyhr/warehouse.py @@ -0,0 +1,14 @@ +from hrflow_connectors.v2.connectors.breezyhr.aisles import ( + AuthParameters, + JobsAisle, + ProfilesAisle, +) +from hrflow_connectors.v2.core.warehouse import Warehouse + +BreezyHrWarehouse = Warehouse( + auth=AuthParameters, + aisles=( + JobsAisle, + ProfilesAisle, + ), +)