diff --git a/.github/workflows/hrflow_connectors.yml b/.github/workflows/hrflow_connectors.yml
index 63ba97166..554ddb75e 100644
--- a/.github/workflows/hrflow_connectors.yml
+++ b/.github/workflows/hrflow_connectors.yml
@@ -159,7 +159,7 @@ jobs:
- name: Run Connector tests
run: |
- poetry run nox -- -s tests -- --no-cov --ignore tests/core --connector=SmartRecruiters --connector=PoleEmploi --connector=Adzuna --connector=Hubspot --connector=Waalaxy
+ poetry run nox -- -s tests -- --no-cov --ignore tests/core --connector=SmartRecruiters --connector=PoleEmploi --connector=Adzuna --connector=Waalaxy
env:
HRFLOW_CONNECTORS_STORE_ENABLED: "1"
HRFLOW_CONNECTORS_LOCALJSON_DIR: "/tmp/"
diff --git a/README.md b/README.md
index 519cfaaf5..59c193d79 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,10 @@
![GitHub Repo stars](https://img.shields.io/github/stars/Riminder/hrflow-connectors?style=social) ![](https://img.shields.io/github/v/release/Riminder/hrflow-connectors) ![](https://img.shields.io/github/license/Riminder/hrflow-connectors)
-
# πΌ About HrFlow.ai
**[HrFlow.ai](https://hrflow.ai/) is on a mission to make AI and data integration pipelines a commodity in the HR Industry:**
+
1. **Unify**: Link your Talent Data channels with a few clicks, so they can share data.
2. **Understand**: Leverage our AI solutions to process your Talent Data.
3. **Automate**: Sync data between your tools and build workflows that meet your business logic.
@@ -25,10 +25,11 @@
-# π» About **HrFlow-Connectors**
+# π» About **HrFlow-Connectors**
+
**HrFlow-Connectors** is an open-source project created by HrFlow.ai to democratize Talent Data integration within the HR Tech landscape.
-We invite developers to join us in our mission to bring AI and data integration to the HR industr, as a developper you can:
+We invite developers to join us in our mission to bring AI and data integration to the HR industr, as a developper you can:
- Create new connectors quickly and easily with our low-code connector approach and abstracted concepts
- Contribute to the Connectors' framework with your own code
@@ -40,9 +41,10 @@ We invite developers to join us in our mission to bring AI and data integration
-π **More instructions are available in the Documentation section below**
+π **More instructions are available in the Documentation section below**
# π€ List of Connectors (ATS/CRM/HCM)
+
| Name | Type | Available | Release date | Last update | Pull profile list action | Pull job list action | Push profile action | Push job action |
|-------------------------------------------------------------------------------------------------------------------------------------------|----------------------|--------------------|----------------|-----------------|---------------------|-----------------|-------------------------|----------------------|
| **ADP** | HCM | π― | | | | | | |
@@ -60,8 +62,8 @@ We invite developers to join us in our mission to bring AI and data integration
| **Comeet** | ATS | π― | | | | | | |
| **Cornerstone OnDemand** | ATS | π― | | | | | | |
| **Crosstalent** | ATS | :hourglass: | *19/01/2022* | | | | | |
-| **Digitalrecruiters** | ATS | π― | | | | | | |
-| **EngageATS** | ATS | π― | | | | | | |
+| [**DigitalRecruiters**](./src/hrflow_connectors/connectors/digitalrecruiters/README.md) | ATS | :white_check_mark: | *17/08/2023* | *23/11/2023* | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
+|**EngageATS** | ATS | π― | | | | | | |
| **EOLIA Software** | ATS | π― | | | | | | |
| **Eploy** | ATS | π― | | | | | | |
| **Fieldglass SAP** | ATS | π― | | | | | | |
@@ -109,12 +111,12 @@ We invite developers to join us in our mission to bring AI and data integration
| [**Workable**](./src/hrflow_connectors/connectors/workable/README.md) | HCM | :white_check_mark: | *27/09/2022* | *30/10/2023* | :x: | :white_check_mark: | :white_check_mark: | :x: |
| **Workday** | HCM | :heavy_check_mark: | | |
-
- :white_check_mark: : Done
- :hourglass: : Work in progress
- π― : Backlog
# π€ List of Connectors (Job Boards)
+
| Name | Type | Available | Release date | Last update | Pull profile list action | Pull job list action | Push profile action | Push job action |
|-------------------------------------------------------------------------------------------------------------------------------------------|----------------------|--------------------|----------------|-----------------|---------------------|-----------------|-------------------------|----------------------|
| **ADENCLASSIFIEDS** | Job Board | π― | | | | | | |
@@ -156,30 +158,37 @@ We invite developers to join us in our mission to bring AI and data integration
| **Welcome To The Jungle** | Job Board | π― | | |
| **Wizbii** | Job Board | π― | | |
| **XML** | Job Board | :hourglass: | | |
+
- :white_check_mark: : Done
- :hourglass: : Work in progress
- π― : Backlog
# πͺ Quickstart
+
## What I can do?
+
With Hrflow Connector, you can **synchronize** and **process** multiple **HR data streams** in just a few lines of code.
You can do any kind of data transfer between HrFlow.ai and external destinations :
-* Pull jobs : `External Job flow` :arrow_right: ***`Hrflow.ai Board`***
-* Pull profiles : `External Profile flow` :arrow_right: ***`Hrflow.ai Source`***
-* Push job : ***`Hrflow.ai Board`*** :arrow_right: `External destination`
-* Push profile : ***`Hrflow.ai Source`*** :arrow_right: `External destination`
+
+- Pull jobs : `External Job flow` :arrow_right: ***`Hrflow.ai Board`***
+- Pull profiles : `External Profile flow` :arrow_right: ***`Hrflow.ai Source`***
+- Push job : ***`Hrflow.ai Board`*** :arrow_right: `External destination`
+- Push profile : ***`Hrflow.ai Source`*** :arrow_right: `External destination`
The features offered by this package:
-* **Synchronize an entire data** stream with a ready-to-use solution
-* **Synchronize only certain data** in a stream meeting a condition defined by you : [`logics`](DOCUMENTATION.md#logics)
-* **Format the data as you wish** or use the default formatting that we propose adapted to each connector : [`format`](DOCUMENTATION.md#format)
-* **Leverage the provider *Hrflow.ai's ** Job and Profile Warehouse * with a many available options like [`hydrate_with_parsing`](src/hrflow_connectors/connectors/hrflow/warehouse.py#L42) or [`update_content`](src/hrflow_connectors/connectors/hrflow/warehouse.py#L39)
+
+- **Synchronize an entire data** stream with a ready-to-use solution
+- **Synchronize only certain data** in a stream meeting a condition defined by you : [`logics`](DOCUMENTATION.md#logics)
+- **Format the data as you wish** or use the default formatting that we propose adapted to each connector : [`format`](DOCUMENTATION.md#format)
+- **Leverage the provider *Hrflow.ai's ** Job and Profile Warehouse* with a many available options like [`hydrate_with_parsing`](src/hrflow_connectors/connectors/hrflow/warehouse.py#L42) or [`update_content`](src/hrflow_connectors/connectors/hrflow/warehouse.py#L39)
## β How to use a connector ?
+
**Prerequisites**
-* [β¨ Create a Workspace](https://hrflow.ai/signup/)
-* [π Get your API Key](https://developers.hrflow.ai/docs/api-authentification)
+
+- [β¨ Create a Workspace](https://hrflow.ai/signup/)
+- [π Get your API Key](https://developers.hrflow.ai/docs/api-authentification)
1. Spin of a terminal shell
2. **`pip install hrflow-connectors`**
@@ -187,8 +196,8 @@ The features offered by this package:
π **TADA! You have just used your first connector.**
-
## π Documentation
+
To find out **more about the HrFlow.ai Connectors framework** take a look at the [π documentation](DOCUMENTATION.md).
## π‘ Contributions
@@ -202,17 +211,22 @@ appreciated.
π **To find out more about how to proceed, the rules and conventions to follow, read carefully [`CONTRIBUTING.md`](CONTRIBUTING.md).**
# π Resources
-* Our Developers documentation : https://developers.hrflow.ai/
-* Our API list (Parsing, Revealing, Embedding, Searching, Scoring, Reasoning) : https://www.hrflow.ai/api
-* Our cool demos labs : https://labs.hrflow.ai
+
+- Our Developers documentation :
+
+- Our API list (Parsing, Revealing, Embedding, Searching, Scoring, Reasoning) :
+- Our cool demos labs :
# π Upcoming Steps and Future Improvements
-The project is now in a stable state, however there are still some features and modifications that can be added to further improve the project.
-**Next features** :
+The project is now in a stable state, however there are still some features and modifications that can be added to further improve the project.
+
+**Next features** :
+
- Add base classes for connectors actions to inherit from when developping new connectors π§
The development team is always open to feedback and new ideas from users, so if you have any suggestions or ideas on how to improve the project, feel free to contact us!
+
# π License
See the [`LICENSE`](LICENSE) file for licensing information.
diff --git a/manifest.json b/manifest.json
index 380053573..643357da3 100644
--- a/manifest.json
+++ b/manifest.json
@@ -21056,6 +21056,2011 @@
],
"type": "CRM",
"logo": "https://raw.githubusercontent.com/Riminder/hrflow-connectors/master/src/hrflow_connectors/connectors/salesforce/logo.jpeg"
+ },
+ {
+ "name": "DigitalRecruiters",
+ "actions": [
+ {
+ "name": "pull_job_list",
+ "action_type": "inbound",
+ "action_parameters": {
+ "title": "ReadJobsActionParameters",
+ "type": "object",
+ "properties": {
+ "read_mode": {
+ "description": "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.",
+ "default": "sync",
+ "allOf": [
+ {
+ "$ref": "#/definitions/ReadMode"
+ }
+ ]
+ },
+ "logics": {
+ "title": "logics",
+ "description": "List of logic functions. Each function should have the following signature typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]. The final list should be exposed in a variable named 'logics'.",
+ "template": "\nimport typing as t\n\ndef logic_1(item: t.Dict) -> t.Union[t.Dict, None]:\n return None\n\ndef logic_2(item: t.Dict) -> t.Uniont[t.Dict, None]:\n return None\n\nlogics = [logic_1, logic_2]\n",
+ "type": "code_editor"
+ },
+ "format": {
+ "title": "format",
+ "description": "Formatting function. You should expose a function named 'format' with following signature typing.Callable[[typing.Dict], typing.Dict]",
+ "template": "\nimport typing as t\n\ndef format(item: t.Dict) -> t.Dict:\n return item\n",
+ "type": "code_editor"
+ }
+ },
+ "additionalProperties": false,
+ "definitions": {
+ "ReadMode": {
+ "title": "ReadMode",
+ "description": "An enumeration.",
+ "enum": [
+ "sync",
+ "incremental"
+ ]
+ }
+ }
+ },
+ "data_type": "job",
+ "trigger_type": "schedule",
+ "origin": "DigitalRecruiters Jobs",
+ "origin_parameters": {
+ "title": "ReadJobsParameters",
+ "type": "object",
+ "properties": {
+ "token": {
+ "title": "Token",
+ "description": "Digital Recruiters API token.",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "environment_url": {
+ "title": "Environment Url",
+ "description": "Digital Recruiters API url environnement.",
+ "field_type": "Other",
+ "type": "string"
+ }
+ },
+ "required": [
+ "token",
+ "environment_url"
+ ],
+ "additionalProperties": false
+ },
+ "origin_data_schema": {
+ "title": "DigitalRecruitersJob",
+ "type": "object",
+ "properties": {
+ "locale": {
+ "title": "Locale",
+ "type": "string"
+ },
+ "reference": {
+ "title": "Reference",
+ "type": "string"
+ },
+ "published_at": {
+ "title": "Published At",
+ "type": "string"
+ },
+ "catch_phrase": {
+ "title": "Catch Phrase",
+ "type": "string"
+ },
+ "contract_type": {
+ "title": "Contract Type",
+ "type": "string"
+ },
+ "contract_duration": {
+ "$ref": "#/definitions/ContractDuration"
+ },
+ "contract_work_period": {
+ "title": "Contract Work Period",
+ "type": "string"
+ },
+ "service": {
+ "title": "Service",
+ "type": "string"
+ },
+ "experience_level": {
+ "title": "Experience Level",
+ "type": "string"
+ },
+ "education_level": {
+ "title": "Education Level",
+ "type": "string"
+ },
+ "title": {
+ "title": "Title",
+ "type": "string"
+ },
+ "description": {
+ "title": "Description",
+ "type": "string"
+ },
+ "profile": {
+ "title": "Profile",
+ "type": "string"
+ },
+ "skills": {
+ "title": "Skills",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "salary": {
+ "$ref": "#/definitions/Salary"
+ },
+ "pictures": {
+ "title": "Pictures",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "videos": {
+ "title": "Videos",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "internal_apply_url": {
+ "title": "Internal Apply Url",
+ "type": "string"
+ },
+ "apply_url": {
+ "title": "Apply Url",
+ "type": "string"
+ },
+ "address": {
+ "$ref": "#/definitions/Address"
+ },
+ "entity": {
+ "$ref": "#/definitions/Entity"
+ },
+ "referent_recruiter": {
+ "$ref": "#/definitions/ReferentRecruiter"
+ },
+ "brand": {
+ "$ref": "#/definitions/Brand"
+ },
+ "custom_fields": {
+ "title": "Custom Fields",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CustomField"
+ }
+ },
+ "count_recruited": {
+ "title": "Count Recruited",
+ "type": "string"
+ }
+ },
+ "required": [
+ "locale",
+ "reference",
+ "published_at",
+ "catch_phrase",
+ "contract_type",
+ "contract_duration",
+ "contract_work_period",
+ "service",
+ "experience_level",
+ "education_level",
+ "title",
+ "description",
+ "profile",
+ "skills",
+ "salary",
+ "pictures",
+ "videos",
+ "address",
+ "entity",
+ "referent_recruiter",
+ "brand",
+ "custom_fields"
+ ],
+ "definitions": {
+ "ContractDuration": {
+ "title": "ContractDuration",
+ "type": "object",
+ "properties": {
+ "min": {
+ "title": "Min",
+ "type": "integer"
+ },
+ "max": {
+ "title": "Max",
+ "type": "integer"
+ }
+ }
+ },
+ "Salary": {
+ "title": "Salary",
+ "type": "object",
+ "properties": {
+ "min": {
+ "title": "Min",
+ "type": "integer"
+ },
+ "max": {
+ "title": "Max",
+ "type": "integer"
+ },
+ "kind": {
+ "title": "Kind",
+ "type": "string"
+ },
+ "rate_type": {
+ "title": "Rate Type",
+ "type": "string"
+ },
+ "variable": {
+ "title": "Variable",
+ "type": "string"
+ },
+ "currency": {
+ "title": "Currency",
+ "type": "string"
+ }
+ }
+ },
+ "AddressParts": {
+ "title": "AddressParts",
+ "type": "object",
+ "properties": {
+ "street": {
+ "title": "Street",
+ "type": "string"
+ },
+ "zip": {
+ "title": "Zip",
+ "type": "string"
+ },
+ "city": {
+ "title": "City",
+ "type": "string"
+ },
+ "county": {
+ "title": "County",
+ "type": "string"
+ },
+ "state": {
+ "title": "State",
+ "type": "string"
+ },
+ "country": {
+ "title": "Country",
+ "type": "string"
+ }
+ },
+ "required": [
+ "street",
+ "zip",
+ "city",
+ "county",
+ "state",
+ "country"
+ ]
+ },
+ "Address": {
+ "title": "Address",
+ "type": "object",
+ "properties": {
+ "parts": {
+ "$ref": "#/definitions/AddressParts"
+ },
+ "formatted": {
+ "title": "Formatted",
+ "type": "string"
+ },
+ "position": {
+ "title": "Position",
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "parts",
+ "formatted",
+ "position"
+ ]
+ },
+ "Manager": {
+ "title": "Manager",
+ "type": "object",
+ "properties": {
+ "section_title": {
+ "title": "Section Title",
+ "type": "string"
+ },
+ "section_body": {
+ "title": "Section Body",
+ "type": "string"
+ },
+ "picture_url": {
+ "title": "Picture Url",
+ "type": "string"
+ },
+ "firstname": {
+ "title": "Firstname",
+ "type": "string"
+ },
+ "lastname": {
+ "title": "Lastname",
+ "type": "string"
+ },
+ "position": {
+ "title": "Position",
+ "type": "string"
+ }
+ },
+ "required": [
+ "section_title",
+ "section_body",
+ "firstname",
+ "lastname",
+ "position"
+ ]
+ },
+ "Hierarchy": {
+ "title": "Hierarchy",
+ "type": "object",
+ "properties": {
+ "depth": {
+ "title": "Depth",
+ "type": "integer"
+ },
+ "column_name": {
+ "title": "Column Name",
+ "type": "string"
+ },
+ "public_name": {
+ "title": "Public Name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "depth",
+ "column_name",
+ "public_name"
+ ]
+ },
+ "Entity": {
+ "title": "Entity",
+ "type": "object",
+ "properties": {
+ "public_name": {
+ "title": "Public Name",
+ "type": "string"
+ },
+ "internal_ref": {
+ "title": "Internal Ref",
+ "type": "string"
+ },
+ "around": {
+ "title": "Around",
+ "type": "string"
+ },
+ "address": {
+ "$ref": "#/definitions/Address"
+ },
+ "manager": {
+ "$ref": "#/definitions/Manager"
+ },
+ "hierarchy": {
+ "title": "Hierarchy",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Hierarchy"
+ }
+ }
+ },
+ "required": [
+ "public_name",
+ "internal_ref",
+ "around",
+ "address",
+ "manager",
+ "hierarchy"
+ ]
+ },
+ "ReferentRecruiter": {
+ "title": "ReferentRecruiter",
+ "type": "object",
+ "properties": {
+ "firstname": {
+ "title": "Firstname",
+ "type": "string"
+ },
+ "lastname": {
+ "title": "Lastname",
+ "type": "string"
+ },
+ "picture_url": {
+ "title": "Picture Url",
+ "type": "string"
+ }
+ },
+ "required": [
+ "firstname",
+ "lastname"
+ ]
+ },
+ "Brand": {
+ "title": "Brand",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "description": {
+ "title": "Description",
+ "type": "string"
+ },
+ "logo": {
+ "title": "Logo",
+ "type": "string"
+ },
+ "favicon": {
+ "title": "Favicon",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "description",
+ "logo",
+ "favicon"
+ ]
+ },
+ "CustomField": {
+ "title": "CustomField",
+ "type": "object",
+ "properties": {
+ "hash": {
+ "title": "Hash",
+ "type": "string"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "type": "string"
+ }
+ },
+ "required": [
+ "hash",
+ "name",
+ "value"
+ ]
+ }
+ }
+ },
+ "supports_incremental": false,
+ "target": "HrFlow.ai Jobs",
+ "target_parameters": {
+ "title": "WriteJobParameters",
+ "type": "object",
+ "properties": {
+ "api_secret": {
+ "title": "Api Secret",
+ "description": "X-API-KEY used to access HrFlow.ai API",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "api_user": {
+ "title": "Api User",
+ "description": "X-USER-EMAIL used to access HrFlow.ai API",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "board_key": {
+ "title": "Board Key",
+ "description": "HrFlow.ai board key",
+ "field_type": "Query Param",
+ "type": "string"
+ },
+ "sync": {
+ "title": "Sync",
+ "description": "When enabled only pushed jobs will remain in the board",
+ "default": true,
+ "field_type": "Other",
+ "type": "boolean"
+ },
+ "update_content": {
+ "title": "Update Content",
+ "description": "When enabled jobs already present in the board are updated",
+ "default": false,
+ "field_type": "Other",
+ "type": "boolean"
+ },
+ "enrich_with_parsing": {
+ "title": "Enrich With Parsing",
+ "description": "When enabled jobs are enriched with HrFlow.ai parsing",
+ "default": false,
+ "field_type": "Other",
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "api_secret",
+ "api_user",
+ "board_key"
+ ],
+ "additionalProperties": false
+ },
+ "target_data_schema": {
+ "title": "HrFlowJob",
+ "type": "object",
+ "properties": {
+ "key": {
+ "title": "Key",
+ "description": "Identification key of the Job.",
+ "type": "string"
+ },
+ "reference": {
+ "title": "Reference",
+ "description": "Custom identifier of the Job.",
+ "type": "string"
+ },
+ "name": {
+ "title": "Name",
+ "description": "Job title.",
+ "type": "string"
+ },
+ "location": {
+ "title": "Location",
+ "description": "Job location object.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Location"
+ }
+ ]
+ },
+ "sections": {
+ "title": "Sections",
+ "description": "Job custom sections.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Section"
+ }
+ },
+ "url": {
+ "title": "Url",
+ "description": "Job post original URL.",
+ "type": "string"
+ },
+ "summary": {
+ "title": "Summary",
+ "description": "Brief summary of the Job.",
+ "type": "string"
+ },
+ "archieved_at": {
+ "title": "Archieved At",
+ "description": "type: datetime ISO8601, Archive date of the Job. The value is null for unarchived Jobs.",
+ "type": "string"
+ },
+ "updated_at": {
+ "title": "Updated At",
+ "description": "type: datetime ISO8601, Last update date of the Job.",
+ "type": "string"
+ },
+ "created_at": {
+ "title": "Created At",
+ "description": "type: datetime ISO8601, Creation date of the Job.",
+ "type": "string"
+ },
+ "skills": {
+ "title": "Skills",
+ "description": "t.List of skills of the Job.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Skill"
+ }
+ },
+ "languages": {
+ "title": "Languages",
+ "description": "t.List of spoken languages of the Job",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "certifications": {
+ "title": "Certifications",
+ "description": "t.List of certifications of the Job.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "courses": {
+ "title": "Courses",
+ "description": "t.List of courses of the Job",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "tasks": {
+ "title": "Tasks",
+ "description": "t.List of tasks of the Job",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "tags": {
+ "title": "Tags",
+ "description": "t.List of tags of the Job",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "metadatas": {
+ "title": "Metadatas",
+ "description": "t.List of metadatas of the Job",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "ranges_float": {
+ "title": "Ranges Float",
+ "description": "t.List of ranges of floats",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/RangesFloat"
+ }
+ },
+ "ranges_date": {
+ "title": "Ranges Date",
+ "description": "t.List of ranges of dates",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/RangesDate"
+ }
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "definitions": {
+ "Location": {
+ "title": "Location",
+ "type": "object",
+ "properties": {
+ "text": {
+ "title": "Text",
+ "description": "Location text address.",
+ "type": "string"
+ },
+ "lat": {
+ "title": "Lat",
+ "description": "Geocentric latitude of the Location.",
+ "type": "number"
+ },
+ "lng": {
+ "title": "Lng",
+ "description": "Geocentric longitude of the Location.",
+ "type": "number"
+ }
+ }
+ },
+ "Section": {
+ "title": "Section",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of a Section of the Job. Example: culture",
+ "type": "string"
+ },
+ "title": {
+ "title": "Title",
+ "description": "Display Title of a Section. Example: Corporate Culture",
+ "type": "string"
+ },
+ "description": {
+ "title": "Description",
+ "description": "Text description of a Section: Example: Our values areNone",
+ "type": "string"
+ }
+ }
+ },
+ "Skill": {
+ "title": "Skill",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of the skill",
+ "type": "string"
+ },
+ "type": {
+ "title": "Type",
+ "description": "Type of the skill. hard or soft",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "description": "Value associated to the skill",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ]
+ },
+ "GeneralEntitySchema": {
+ "title": "GeneralEntitySchema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of the Object",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "description": "Value associated to the Object's name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ]
+ },
+ "RangesFloat": {
+ "title": "RangesFloat",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of a Range of floats attached to the Job. Example: salary",
+ "type": "string"
+ },
+ "value_min": {
+ "title": "Value Min",
+ "description": "Min value. Example: 500.",
+ "type": "number"
+ },
+ "value_max": {
+ "title": "Value Max",
+ "description": "Max value. Example: 100.",
+ "type": "number"
+ },
+ "unit": {
+ "title": "Unit",
+ "description": "Unit of the value. Example: euros.",
+ "type": "string"
+ }
+ }
+ },
+ "RangesDate": {
+ "title": "RangesDate",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of a Range of dates attached to the Job. Example: availability.",
+ "type": "string"
+ },
+ "value_min": {
+ "title": "Value Min",
+ "description": "Min value in datetime ISO 8601, Example: 500.",
+ "type": "string"
+ },
+ "value_max": {
+ "title": "Value Max",
+ "description": "Max value in datetime ISO 8601, Example: 1000",
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "workflow_code": "import typing as t\n\nfrom hrflow_connectors import DigitalRecruiters\nfrom hrflow_connectors.core.connector import ActionInitError, Reason\n\nORIGIN_SETTINGS_PREFIX = \"origin_\"\nTARGET_SETTINGS_PREFIX = \"target_\"\n\n# << format_placeholder >>\n\n# << logics_placeholder >>\n\n\ndef workflow(\n \n settings: t.Dict\n ) -> None:\n actions_parameters = dict()\n try:\n format\n except NameError:\n pass\n else:\n actions_parameters[\"format\"] = format\n\n try:\n logics\n except NameError:\n pass\n else:\n actions_parameters[\"logics\"] = logics\n\n if \"__workflow_id\" not in settings:\n return DigitalRecruiters.pull_job_list(\n workflow_id=\"\",\n action_parameters=dict(),\n origin_parameters=dict(),\n target_parameters=dict(),\n init_error=ActionInitError(\n reason=Reason.workflow_id_not_found,\n data=dict(error=\"__workflow_id not found in settings\", settings_keys=list(settings.keys())),\n )\n )\n workflow_id = settings[\"__workflow_id\"]\n\n \n\n origin_parameters = dict()\n for parameter in ['token', 'environment_url']:\n if \"{}{}\".format(ORIGIN_SETTINGS_PREFIX, parameter) in settings:\n origin_parameters[parameter] = settings[\"{}{}\".format(ORIGIN_SETTINGS_PREFIX, parameter)]\n \n\n target_parameters = dict()\n for parameter in ['api_secret', 'api_user', 'board_key', 'sync', 'update_content', 'enrich_with_parsing']:\n if \"{}{}\".format(TARGET_SETTINGS_PREFIX, parameter) in settings:\n target_parameters[parameter] = settings[\"{}{}\".format(TARGET_SETTINGS_PREFIX, parameter)]\n \n\n return DigitalRecruiters.pull_job_list(\n workflow_id=workflow_id,\n action_parameters=actions_parameters,\n origin_parameters=origin_parameters,\n target_parameters=target_parameters,\n )",
+ "workflow_code_format_placeholder": "# << format_placeholder >>",
+ "workflow_code_logics_placeholder": "# << logics_placeholder >>",
+ "workflow_code_workflow_id_settings_key": "__workflow_id",
+ "workflow_code_origin_settings_prefix": "origin_",
+ "workflow_code_target_settings_prefix": "target_"
+ },
+ {
+ "name": "pull_profile_list",
+ "action_type": "inbound",
+ "action_parameters": {
+ "title": "ReadProfilesActionParameters",
+ "type": "object",
+ "properties": {
+ "read_mode": {
+ "description": "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.",
+ "default": "sync",
+ "allOf": [
+ {
+ "$ref": "#/definitions/ReadMode"
+ }
+ ]
+ },
+ "logics": {
+ "title": "logics",
+ "description": "List of logic functions. Each function should have the following signature typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]. The final list should be exposed in a variable named 'logics'.",
+ "template": "\nimport typing as t\n\ndef logic_1(item: t.Dict) -> t.Union[t.Dict, None]:\n return None\n\ndef logic_2(item: t.Dict) -> t.Uniont[t.Dict, None]:\n return None\n\nlogics = [logic_1, logic_2]\n",
+ "type": "code_editor"
+ },
+ "format": {
+ "title": "format",
+ "description": "Formatting function. You should expose a function named 'format' with following signature typing.Callable[[typing.Dict], typing.Dict]",
+ "template": "\nimport typing as t\n\ndef format(item: t.Dict) -> t.Dict:\n return item\n",
+ "type": "code_editor"
+ }
+ },
+ "additionalProperties": false,
+ "definitions": {
+ "ReadMode": {
+ "title": "ReadMode",
+ "description": "An enumeration.",
+ "enum": [
+ "sync",
+ "incremental"
+ ]
+ }
+ }
+ },
+ "data_type": "profile",
+ "trigger_type": "schedule",
+ "origin": "DigitalRecruiters Read Profils",
+ "origin_parameters": {
+ "title": "ReadProfileParameters",
+ "type": "object",
+ "properties": {
+ "api_key": {
+ "title": "Api Key",
+ "description": "DigitalRecruiters API key",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "username": {
+ "title": "Username",
+ "description": "Username for authentication",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "password": {
+ "title": "Password",
+ "description": "Password for authentication",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "environment_url": {
+ "title": "Environment Url",
+ "description": "URL environment for the API",
+ "field_type": "Other",
+ "minLength": 1,
+ "maxLength": 2083,
+ "format": "uri",
+ "type": "string"
+ },
+ "jobAd": {
+ "title": "Jobad",
+ "description": "Optional: Id of a job advertisement",
+ "field_type": "Other",
+ "type": "integer"
+ },
+ "sort": {
+ "title": "Sort",
+ "description": "Optional: Field to sort by (id, firstName, lastName, createdAt, updatedAt)",
+ "field_type": "Other",
+ "type": "string"
+ },
+ "limit": {
+ "title": "Limit",
+ "description": "Optional: Limit the number of results returned",
+ "default": 50,
+ "field_type": "Other",
+ "type": "integer"
+ },
+ "page": {
+ "title": "Page",
+ "description": "Optional: Page number of results returned",
+ "default": 1,
+ "field_type": "Other",
+ "type": "integer"
+ }
+ },
+ "required": [
+ "api_key",
+ "username",
+ "password",
+ "environment_url"
+ ],
+ "additionalProperties": false
+ },
+ "origin_data_schema": {
+ "title": "DigitalRecruitersReadProfile",
+ "type": "object",
+ "properties": {
+ "id": {
+ "title": "Id",
+ "type": "integer"
+ },
+ "firstName": {
+ "title": "Firstname",
+ "type": "string"
+ },
+ "lastName": {
+ "title": "Lastname",
+ "type": "string"
+ },
+ "createdAt": {
+ "title": "Createdat",
+ "type": "string",
+ "format": "date-time"
+ },
+ "jobTitle": {
+ "title": "Jobtitle",
+ "type": "string"
+ },
+ "avatar": {
+ "$ref": "#/definitions/Avatar"
+ },
+ "gender": {
+ "title": "Gender",
+ "type": "string"
+ },
+ "email": {
+ "title": "Email",
+ "type": "string"
+ },
+ "location": {
+ "$ref": "#/definitions/Location"
+ },
+ "contract": {
+ "$ref": "#/definitions/ContractItem"
+ },
+ "status": {
+ "title": "Status",
+ "type": "string"
+ },
+ "jobReference": {
+ "$ref": "#/definitions/JobReference"
+ },
+ "privacy": {
+ "$ref": "#/definitions/Privacy"
+ },
+ "cv": {
+ "$ref": "#/definitions/CV"
+ },
+ "resume": {
+ "$ref": "#/definitions/Resume"
+ }
+ },
+ "required": [
+ "id",
+ "firstName",
+ "lastName",
+ "createdAt",
+ "jobTitle",
+ "avatar",
+ "gender",
+ "email",
+ "location",
+ "contract",
+ "status",
+ "jobReference",
+ "privacy",
+ "cv"
+ ],
+ "definitions": {
+ "Avatar": {
+ "title": "Avatar",
+ "type": "object",
+ "properties": {
+ "url": {
+ "title": "Url",
+ "minLength": 1,
+ "maxLength": 2083,
+ "format": "uri",
+ "type": "string"
+ }
+ },
+ "required": [
+ "url"
+ ]
+ },
+ "Location": {
+ "title": "Location",
+ "type": "object",
+ "properties": {
+ "zip": {
+ "title": "Zip",
+ "type": "string"
+ },
+ "city": {
+ "title": "City",
+ "type": "string"
+ },
+ "county": {
+ "title": "County",
+ "type": "string"
+ },
+ "state": {
+ "title": "State",
+ "type": "string"
+ },
+ "country": {
+ "title": "Country",
+ "type": "string"
+ },
+ "latitude": {
+ "title": "Latitude",
+ "type": "number"
+ },
+ "longitude": {
+ "title": "Longitude",
+ "type": "number"
+ }
+ },
+ "required": [
+ "zip",
+ "city",
+ "country"
+ ]
+ },
+ "ContractItem": {
+ "title": "ContractItem",
+ "type": "object",
+ "properties": {
+ "id": {
+ "title": "Id",
+ "type": "integer"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "countryNodeIds": {
+ "title": "Countrynodeids",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ]
+ },
+ "JobReference": {
+ "title": "JobReference",
+ "type": "object",
+ "properties": {
+ "label": {
+ "title": "Label",
+ "type": "string"
+ },
+ "hashId": {
+ "title": "Hashid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "label",
+ "hashId"
+ ]
+ },
+ "Privacy": {
+ "title": "Privacy",
+ "type": "object",
+ "properties": {
+ "status": {
+ "title": "Status",
+ "type": "string"
+ },
+ "updatedAt": {
+ "title": "Updatedat",
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "status"
+ ]
+ },
+ "CV": {
+ "title": "CV",
+ "type": "object",
+ "properties": {
+ "url": {
+ "title": "Url",
+ "minLength": 1,
+ "maxLength": 2083,
+ "format": "uri",
+ "type": "string"
+ }
+ },
+ "required": [
+ "url"
+ ]
+ },
+ "Resume": {
+ "title": "Resume",
+ "type": "object",
+ "properties": {
+ "raw": {
+ "title": "Raw",
+ "type": "string",
+ "format": "binary"
+ },
+ "content_type": {
+ "title": "Content Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "raw",
+ "content_type"
+ ]
+ }
+ }
+ },
+ "supports_incremental": false,
+ "target": "HrFlow.ai Profile Parsing",
+ "target_parameters": {
+ "title": "WriteProfileParsingParameters",
+ "type": "object",
+ "properties": {
+ "api_secret": {
+ "title": "Api Secret",
+ "description": "X-API-KEY used to access HrFlow.ai API",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "api_user": {
+ "title": "Api User",
+ "description": "X-USER-EMAIL used to access HrFlow.ai API",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "source_key": {
+ "title": "Source Key",
+ "description": "HrFlow.ai source key",
+ "field_type": "Other",
+ "type": "string"
+ },
+ "only_insert": {
+ "title": "Only Insert",
+ "description": "When enabled the profile is written only if it doesn't exist in the source",
+ "default": false,
+ "field_type": "Other",
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "api_secret",
+ "api_user",
+ "source_key"
+ ],
+ "additionalProperties": false
+ },
+ "target_data_schema": {
+ "title": "HrFlowProfileParsing",
+ "type": "object",
+ "properties": {
+ "reference": {
+ "title": "Reference",
+ "description": "Custom identifier of the Profile.",
+ "type": "string"
+ },
+ "created_at": {
+ "title": "Created At",
+ "description": "type: datetime ISO8601, Creation date of the Profile.",
+ "type": "string"
+ },
+ "resume": {
+ "$ref": "#/definitions/ResumeToParse"
+ },
+ "tags": {
+ "title": "Tags",
+ "description": "List of tags of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "metadatas": {
+ "title": "Metadatas",
+ "description": "List of metadatas of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ }
+ },
+ "required": [
+ "reference",
+ "created_at",
+ "resume",
+ "tags",
+ "metadatas"
+ ],
+ "definitions": {
+ "ResumeToParse": {
+ "title": "ResumeToParse",
+ "type": "object",
+ "properties": {
+ "raw": {
+ "title": "Raw",
+ "type": "string",
+ "format": "binary"
+ },
+ "content_type": {
+ "title": "Content Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "raw",
+ "content_type"
+ ]
+ },
+ "GeneralEntitySchema": {
+ "title": "GeneralEntitySchema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of the Object",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "description": "Value associated to the Object's name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ]
+ }
+ }
+ },
+ "workflow_code": "import typing as t\n\nfrom hrflow_connectors import DigitalRecruiters\nfrom hrflow_connectors.core.connector import ActionInitError, Reason\n\nORIGIN_SETTINGS_PREFIX = \"origin_\"\nTARGET_SETTINGS_PREFIX = \"target_\"\n\n# << format_placeholder >>\n\n# << logics_placeholder >>\n\n\ndef workflow(\n \n settings: t.Dict\n ) -> None:\n actions_parameters = dict()\n try:\n format\n except NameError:\n pass\n else:\n actions_parameters[\"format\"] = format\n\n try:\n logics\n except NameError:\n pass\n else:\n actions_parameters[\"logics\"] = logics\n\n if \"__workflow_id\" not in settings:\n return DigitalRecruiters.pull_profile_list(\n workflow_id=\"\",\n action_parameters=dict(),\n origin_parameters=dict(),\n target_parameters=dict(),\n init_error=ActionInitError(\n reason=Reason.workflow_id_not_found,\n data=dict(error=\"__workflow_id not found in settings\", settings_keys=list(settings.keys())),\n )\n )\n workflow_id = settings[\"__workflow_id\"]\n\n \n\n origin_parameters = dict()\n for parameter in ['api_key', 'username', 'password', 'environment_url', 'jobAd', 'sort', 'limit', 'page']:\n if \"{}{}\".format(ORIGIN_SETTINGS_PREFIX, parameter) in settings:\n origin_parameters[parameter] = settings[\"{}{}\".format(ORIGIN_SETTINGS_PREFIX, parameter)]\n \n\n target_parameters = dict()\n for parameter in ['api_secret', 'api_user', 'source_key', 'only_insert']:\n if \"{}{}\".format(TARGET_SETTINGS_PREFIX, parameter) in settings:\n target_parameters[parameter] = settings[\"{}{}\".format(TARGET_SETTINGS_PREFIX, parameter)]\n \n\n return DigitalRecruiters.pull_profile_list(\n workflow_id=workflow_id,\n action_parameters=actions_parameters,\n origin_parameters=origin_parameters,\n target_parameters=target_parameters,\n )",
+ "workflow_code_format_placeholder": "# << format_placeholder >>",
+ "workflow_code_logics_placeholder": "# << logics_placeholder >>",
+ "workflow_code_workflow_id_settings_key": "__workflow_id",
+ "workflow_code_origin_settings_prefix": "origin_",
+ "workflow_code_target_settings_prefix": "target_"
+ },
+ {
+ "name": "push_profile",
+ "action_type": "outbound",
+ "action_parameters": {
+ "title": "WriteProfilesActionParameters",
+ "type": "object",
+ "properties": {
+ "read_mode": {
+ "description": "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.",
+ "default": "sync",
+ "allOf": [
+ {
+ "$ref": "#/definitions/ReadMode"
+ }
+ ]
+ },
+ "logics": {
+ "title": "logics",
+ "description": "List of logic functions. Each function should have the following signature typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]. The final list should be exposed in a variable named 'logics'.",
+ "template": "\nimport typing as t\n\ndef logic_1(item: t.Dict) -> t.Union[t.Dict, None]:\n return None\n\ndef logic_2(item: t.Dict) -> t.Uniont[t.Dict, None]:\n return None\n\nlogics = [logic_1, logic_2]\n",
+ "type": "code_editor"
+ },
+ "format": {
+ "title": "format",
+ "description": "Formatting function. You should expose a function named 'format' with following signature typing.Callable[[typing.Dict], typing.Dict]",
+ "template": "\nimport typing as t\n\ndef format(item: t.Dict) -> t.Dict:\n return item\n",
+ "type": "code_editor"
+ },
+ "event_parser": {
+ "title": "event_parser",
+ "description": "Event parsing function for **CATCH** integrations. You should expose a function named 'event_parser' with following signature typing.Callable[[typing.Dict], typing.Dict]",
+ "template": "\nimport typing as t\n\ndef event_parser(event: t.Dict) -> t.Dict:\n parsed = dict()\n parsed[\"user_id\"] = event[\"email\"]\n parsed[\"thread_id\"] = event[\"subscription_id\"]\n return parsed\n",
+ "type": "code_editor"
+ }
+ },
+ "additionalProperties": false,
+ "definitions": {
+ "ReadMode": {
+ "title": "ReadMode",
+ "description": "An enumeration.",
+ "enum": [
+ "sync",
+ "incremental"
+ ]
+ }
+ }
+ },
+ "data_type": "profile",
+ "trigger_type": "hook",
+ "origin": "HrFlow.ai Profiles",
+ "origin_parameters": {
+ "title": "ReadProfileParameters",
+ "type": "object",
+ "properties": {
+ "api_secret": {
+ "title": "Api Secret",
+ "description": "X-API-KEY used to access HrFlow.ai API",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "api_user": {
+ "title": "Api User",
+ "description": "X-USER-EMAIL used to access HrFlow.ai API",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "source_key": {
+ "title": "Source Key",
+ "description": "HrFlow.ai source key",
+ "field_type": "Query Param",
+ "type": "string"
+ },
+ "profile_key": {
+ "title": "Profile Key",
+ "description": "HrFlow.ai profile key",
+ "field_type": "Query Param",
+ "type": "string"
+ }
+ },
+ "required": [
+ "api_secret",
+ "api_user",
+ "source_key",
+ "profile_key"
+ ],
+ "additionalProperties": false
+ },
+ "origin_data_schema": {
+ "title": "HrFlowProfile",
+ "type": "object",
+ "properties": {
+ "key": {
+ "title": "Key",
+ "description": "Identification key of the Profile.",
+ "type": "string"
+ },
+ "reference": {
+ "title": "Reference",
+ "description": "Custom identifier of the Profile.",
+ "type": "string"
+ },
+ "archived_at": {
+ "title": "Archived At",
+ "description": "type: datetime ISO8601, Archive date of the Profile. The value is null for unarchived Profiles.",
+ "type": "string"
+ },
+ "updated_at": {
+ "title": "Updated At",
+ "description": "type: datetime ISO8601, Last update date of the Profile.",
+ "type": "string"
+ },
+ "created_at": {
+ "title": "Created At",
+ "description": "type: datetime ISO8601, Creation date of the Profile.",
+ "type": "string"
+ },
+ "info": {
+ "title": "Info",
+ "description": "Object containing the Profile's info.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/ProfileInfo"
+ }
+ ]
+ },
+ "text_language": {
+ "title": "Text Language",
+ "description": "Code language of the Profile. type: string code ISO 639-1",
+ "type": "string"
+ },
+ "text": {
+ "title": "Text",
+ "description": "Full text of the Profile..",
+ "type": "string"
+ },
+ "experiences_duration": {
+ "title": "Experiences Duration",
+ "description": "Total number of years of experience.",
+ "type": "number"
+ },
+ "educations_duration": {
+ "title": "Educations Duration",
+ "description": "Total number of years of education.",
+ "type": "number"
+ },
+ "experiences": {
+ "title": "Experiences",
+ "description": "List of experiences of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Experience"
+ }
+ },
+ "educations": {
+ "title": "Educations",
+ "description": "List of educations of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Education"
+ }
+ },
+ "attachments": {
+ "title": "Attachments",
+ "description": "List of documents attached to the Profile.",
+ "type": "array",
+ "items": {}
+ },
+ "skills": {
+ "title": "Skills",
+ "description": "List of skills of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Skill"
+ }
+ },
+ "languages": {
+ "title": "Languages",
+ "description": "List of spoken languages of the profile",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "certifications": {
+ "title": "Certifications",
+ "description": "List of certifications of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "courses": {
+ "title": "Courses",
+ "description": "List of courses of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "tasks": {
+ "title": "Tasks",
+ "description": "List of tasks of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "interests": {
+ "title": "Interests",
+ "description": "List of interests of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "labels": {
+ "title": "Labels",
+ "description": "List of labels of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "tags": {
+ "title": "Tags",
+ "description": "List of tags of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "metadatas": {
+ "title": "Metadatas",
+ "description": "List of metadatas of the Profile.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ }
+ },
+ "definitions": {
+ "Location": {
+ "title": "Location",
+ "type": "object",
+ "properties": {
+ "text": {
+ "title": "Text",
+ "description": "Location text address.",
+ "type": "string"
+ },
+ "lat": {
+ "title": "Lat",
+ "description": "Geocentric latitude of the Location.",
+ "type": "number"
+ },
+ "lng": {
+ "title": "Lng",
+ "description": "Geocentric longitude of the Location.",
+ "type": "number"
+ }
+ }
+ },
+ "InfoUrls": {
+ "title": "InfoUrls",
+ "type": "object",
+ "properties": {
+ "from_resume": {
+ "title": "From Resume",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "linkedin": {
+ "title": "Linkedin",
+ "type": "string"
+ },
+ "twitter": {
+ "title": "Twitter",
+ "type": "string"
+ },
+ "facebook": {
+ "title": "Facebook",
+ "type": "string"
+ },
+ "github": {
+ "title": "Github",
+ "type": "string"
+ }
+ }
+ },
+ "ProfileInfo": {
+ "title": "ProfileInfo",
+ "type": "object",
+ "properties": {
+ "full_name": {
+ "title": "Full Name",
+ "type": "string"
+ },
+ "first_name": {
+ "title": "First Name",
+ "type": "string"
+ },
+ "last_name": {
+ "title": "Last Name",
+ "type": "string"
+ },
+ "email": {
+ "title": "Email",
+ "type": "string"
+ },
+ "phone": {
+ "title": "Phone",
+ "type": "string"
+ },
+ "date_birth": {
+ "title": "Date Birth",
+ "description": "Profile date of birth",
+ "type": "string"
+ },
+ "location": {
+ "title": "Location",
+ "description": "Profile location object",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Location"
+ }
+ ]
+ },
+ "urls": {
+ "title": "Urls",
+ "description": "Profile social networks and URLs",
+ "allOf": [
+ {
+ "$ref": "#/definitions/InfoUrls"
+ }
+ ]
+ },
+ "picture": {
+ "title": "Picture",
+ "description": "Profile picture url",
+ "type": "string"
+ },
+ "gender": {
+ "title": "Gender",
+ "description": "Profile gender",
+ "type": "string"
+ },
+ "summary": {
+ "title": "Summary",
+ "description": "Profile summary text",
+ "type": "string"
+ }
+ }
+ },
+ "Skill": {
+ "title": "Skill",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of the skill",
+ "type": "string"
+ },
+ "type": {
+ "title": "Type",
+ "description": "Type of the skill. hard or soft",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "description": "Value associated to the skill",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ]
+ },
+ "GeneralEntitySchema": {
+ "title": "GeneralEntitySchema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "Identification name of the Object",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "description": "Value associated to the Object's name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ]
+ },
+ "Experience": {
+ "title": "Experience",
+ "type": "object",
+ "properties": {
+ "key": {
+ "title": "Key",
+ "description": "Identification key of the Experience.",
+ "type": "string"
+ },
+ "company": {
+ "title": "Company",
+ "description": "Company name of the Experience.",
+ "type": "string"
+ },
+ "title": {
+ "title": "Title",
+ "description": "Title of the Experience.",
+ "type": "string"
+ },
+ "description": {
+ "title": "Description",
+ "description": "Description of the Experience.",
+ "type": "string"
+ },
+ "location": {
+ "title": "Location",
+ "description": "Location object of the Experience.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Location"
+ }
+ ]
+ },
+ "date_start": {
+ "title": "Date Start",
+ "description": "Start date of the experience. type: ('datetime ISO 8601')",
+ "type": "string"
+ },
+ "date_end": {
+ "title": "Date End",
+ "description": "End date of the experience. type: ('datetime ISO 8601')",
+ "type": "string"
+ },
+ "skills": {
+ "title": "Skills",
+ "description": "List of skills of the Experience.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Skill"
+ }
+ },
+ "certifications": {
+ "title": "Certifications",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "courses": {
+ "title": "Courses",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "tasks": {
+ "title": "Tasks",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ }
+ }
+ },
+ "Education": {
+ "title": "Education",
+ "type": "object",
+ "properties": {
+ "key": {
+ "title": "Key",
+ "description": "Identification key of the Education.",
+ "type": "string"
+ },
+ "school": {
+ "title": "School",
+ "description": "School name of the Education.",
+ "type": "string"
+ },
+ "title": {
+ "title": "Title",
+ "description": "Title of the Education.",
+ "type": "string"
+ },
+ "description": {
+ "title": "Description",
+ "description": "Description of the Education.",
+ "type": "string"
+ },
+ "location": {
+ "title": "Location",
+ "description": "Location object of the Education.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Location"
+ }
+ ]
+ },
+ "date_start": {
+ "title": "Date Start",
+ "description": "Start date of the Education. type: ('datetime ISO 8601')",
+ "type": "string"
+ },
+ "date_end": {
+ "title": "Date End",
+ "description": "End date of the Education. type: ('datetime ISO 8601')",
+ "type": "string"
+ },
+ "skills": {
+ "title": "Skills",
+ "description": "List of skills of the Education.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Skill"
+ }
+ },
+ "certifications": {
+ "title": "Certifications",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "courses": {
+ "title": "Courses",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ },
+ "tasks": {
+ "title": "Tasks",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeneralEntitySchema"
+ }
+ }
+ }
+ }
+ }
+ },
+ "supports_incremental": false,
+ "target": "DigitalRecruiters Write Profile",
+ "target_parameters": {
+ "title": "WriteProfilesParameters",
+ "type": "object",
+ "properties": {
+ "token": {
+ "title": "Token",
+ "description": "Digital Recruiters API token.",
+ "field_type": "Auth",
+ "type": "string"
+ },
+ "environment_url": {
+ "title": "Environment Url",
+ "description": "Digital Recruiters API url environnement.",
+ "field_type": "Other",
+ "type": "string"
+ },
+ "job_reference": {
+ "title": "Job Reference",
+ "description": "reference of the job to which the candidate is applying.",
+ "field_type": "Other",
+ "type": "string"
+ },
+ "message": {
+ "title": "Message",
+ "description": "Application message.",
+ "default": "message du candidat",
+ "field_type": "Other",
+ "type": "string"
+ }
+ },
+ "required": [
+ "token",
+ "environment_url",
+ "job_reference"
+ ],
+ "additionalProperties": false
+ },
+ "target_data_schema": {
+ "title": "DigitalRecruitersWriteProfile",
+ "type": "object",
+ "properties": {
+ "reference": {
+ "title": "Reference",
+ "type": "string"
+ },
+ "consent_date": {
+ "title": "Consent Date",
+ "type": "string"
+ },
+ "s_o": {
+ "title": "S O",
+ "type": "string"
+ },
+ "locale": {
+ "title": "Locale",
+ "type": "string"
+ },
+ "ApplicationMessage": {
+ "$ref": "#/definitions/DigitalRecruitersImportCandidateMessage"
+ },
+ "ApplicationProfile": {
+ "$ref": "#/definitions/DigitalRecruitersCandidateProfile"
+ },
+ "file": {
+ "$ref": "#/definitions/DigitalRecruitersImportCandidateFile"
+ }
+ },
+ "required": [
+ "reference",
+ "consent_date",
+ "s_o",
+ "locale",
+ "ApplicationMessage",
+ "ApplicationProfile"
+ ],
+ "definitions": {
+ "DigitalRecruitersImportCandidateMessage": {
+ "title": "DigitalRecruitersImportCandidateMessage",
+ "type": "object",
+ "properties": {
+ "message": {
+ "title": "Message",
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ]
+ },
+ "DigitalRecruitersCandidateProfile": {
+ "title": "DigitalRecruitersCandidateProfile",
+ "type": "object",
+ "properties": {
+ "gender": {
+ "title": "Gender",
+ "type": "integer"
+ },
+ "firstName": {
+ "title": "Firstname",
+ "type": "string"
+ },
+ "lastName": {
+ "title": "Lastname",
+ "type": "string"
+ },
+ "email": {
+ "title": "Email",
+ "type": "string"
+ },
+ "phoneNumber": {
+ "title": "Phonenumber",
+ "type": "string"
+ },
+ "job": {
+ "title": "Job",
+ "type": "string"
+ },
+ "addressStreet": {
+ "title": "Addressstreet",
+ "type": "string"
+ },
+ "addressZip": {
+ "title": "Addresszip",
+ "type": "string"
+ },
+ "addressCity": {
+ "title": "Addresscity",
+ "type": "string"
+ }
+ },
+ "required": [
+ "gender",
+ "firstName",
+ "lastName",
+ "email"
+ ]
+ },
+ "DigitalRecruitersImportCandidateFile": {
+ "title": "DigitalRecruitersImportCandidateFile",
+ "type": "object",
+ "properties": {
+ "content": {
+ "title": "Content",
+ "type": "string"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "content",
+ "name"
+ ]
+ }
+ }
+ },
+ "workflow_code": "import typing as t\n\nfrom hrflow_connectors import DigitalRecruiters\nfrom hrflow_connectors.core.connector import ActionInitError, Reason\n\nORIGIN_SETTINGS_PREFIX = \"origin_\"\nTARGET_SETTINGS_PREFIX = \"target_\"\n\n# << format_placeholder >>\n\n# << logics_placeholder >>\n\n# << event_parser_placeholder >>\n\n\ndef workflow(\n \n _request: t.Dict,\n \n settings: t.Dict\n ) -> None:\n actions_parameters = dict()\n try:\n format\n except NameError:\n pass\n else:\n actions_parameters[\"format\"] = format\n\n try:\n logics\n except NameError:\n pass\n else:\n actions_parameters[\"logics\"] = logics\n\n if \"__workflow_id\" not in settings:\n return DigitalRecruiters.push_profile(\n workflow_id=\"\",\n action_parameters=dict(),\n origin_parameters=dict(),\n target_parameters=dict(),\n init_error=ActionInitError(\n reason=Reason.workflow_id_not_found,\n data=dict(error=\"__workflow_id not found in settings\", settings_keys=list(settings.keys())),\n )\n )\n workflow_id = settings[\"__workflow_id\"]\n\n \n try:\n event_parser\n _event_parser = event_parser\n except NameError as e:\n action = DigitalRecruiters.model.action_by_name(\"push_profile\")\n # Without this trick event_parser is always only fetched from the local scope\n # meaning that try block always raises NameError even if the function is\n # defined in the placeholder\n _event_parser = action.parameters.__fields__[\"event_parser\"].default\n\n if _event_parser is not None:\n try:\n _request = _event_parser(_request)\n except Exception as e:\n return DigitalRecruiters.push_profile(\n workflow_id=workflow_id,\n action_parameters=dict(),\n origin_parameters=dict(),\n target_parameters=dict(),\n init_error=ActionInitError(\n reason=Reason.event_parsing_failure,\n data=dict(error=e, event=_request),\n )\n )\n \n\n origin_parameters = dict()\n for parameter in ['api_secret', 'api_user', 'source_key', 'profile_key']:\n if \"{}{}\".format(ORIGIN_SETTINGS_PREFIX, parameter) in settings:\n origin_parameters[parameter] = settings[\"{}{}\".format(ORIGIN_SETTINGS_PREFIX, parameter)]\n \n if parameter in _request:\n origin_parameters[parameter] = _request[parameter]\n \n\n target_parameters = dict()\n for parameter in ['token', 'environment_url', 'job_reference', 'message']:\n if \"{}{}\".format(TARGET_SETTINGS_PREFIX, parameter) in settings:\n target_parameters[parameter] = settings[\"{}{}\".format(TARGET_SETTINGS_PREFIX, parameter)]\n \n if parameter in _request:\n target_parameters[parameter] = _request[parameter]\n \n\n return DigitalRecruiters.push_profile(\n workflow_id=workflow_id,\n action_parameters=actions_parameters,\n origin_parameters=origin_parameters,\n target_parameters=target_parameters,\n )",
+ "workflow_code_format_placeholder": "# << format_placeholder >>",
+ "workflow_code_logics_placeholder": "# << logics_placeholder >>",
+ "workflow_code_event_parser_placeholder": "# << event_parser_placeholder >>",
+ "workflow_code_workflow_id_settings_key": "__workflow_id",
+ "workflow_code_origin_settings_prefix": "origin_",
+ "workflow_code_target_settings_prefix": "target_"
+ }
+ ],
+ "type": "ATS",
+ "logo": "https://raw.githubusercontent.com/Riminder/hrflow-connectors/master/src/hrflow_connectors/connectors/digitalrecruiters/logo.png"
}
]
}
\ No newline at end of file
diff --git a/src/hrflow_connectors/__init__.py b/src/hrflow_connectors/__init__.py
index caac5ce27..bb6e4b938 100644
--- a/src/hrflow_connectors/__init__.py
+++ b/src/hrflow_connectors/__init__.py
@@ -2,6 +2,7 @@
from hrflow_connectors.connectors.breezyhr import BreezyHR
from hrflow_connectors.connectors.bullhorn import Bullhorn
from hrflow_connectors.connectors.ceridian import Ceridian
+from hrflow_connectors.connectors.digitalrecruiters import DigitalRecruiters
from hrflow_connectors.connectors.greenhouse.connector import Greenhouse
from hrflow_connectors.connectors.hubspot import Hubspot
from hrflow_connectors.connectors.poleemploi import PoleEmploi
@@ -36,6 +37,7 @@
Hubspot,
Taleez,
Salesforce,
+ DigitalRecruiters,
]
# This makes sure that connector are in module namespace
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/README.md b/src/hrflow_connectors/connectors/digitalrecruiters/README.md
new file mode 100644
index 000000000..df5c45c15
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/README.md
@@ -0,0 +1,69 @@
+# π Summary
+
+- [π Summary](#-summary)
+- [πΌ About Digitalrecruiters](#-about-digitalrecruiters)
+ - [π Why is it a big deal for Digitalrecruiters customers & partners?](#-why-is-it-a-big-deal-for-digitalrecruiters-customers--partners)
+- [π§ How does it work?](#-how-does-it-work)
+ - [π Data integration capabilities:](#-data-integration-capabilities)
+- [π Connector Actions](#-connector-actions)
+- [π Quick Start Examples](#-quick-start-examples)
+- [π Useful Links](#-useful-links)
+- [π Special Thanks](#-special-thanks)
+
+# πΌ About Digitalrecruiters
+
+> Digital Recruiters: Tech-driven hiring platform with job posting, automation, and analytics. Simplify recruitment, reduce time-to-hire, and elevate candidate experience. Streamline the hiring process for businesses with advanced features and integration capabilities.
+
+## π Why is it a big deal for Digitalrecruiters customers & partners?
+
+This new connector will enable:
+
+- β‘ A Fastlane Talent & Workforce data integration for Digitalrecruiters customers & partners
+- π€ Cutting-edge AI-powered Talent Experiences & Recruiter Experiences for Digitalrecruiters customers
+
+# π§ How does it work?
+
+## π Data integration capabilities
+
+- β¬
οΈ Send Profiles data from Digitalrecruiters to a Destination of your choice.
+- β‘οΈ Send Profiles data from a Source of your choice to Digitalrecruiters.
+- β¬
οΈ Send Jobs data from Digitalrecruiters to a Destination of your choice.
+
+# π Connector Actions
+
+
+| Action | Description |
+| ------- | ----------- |
+| [**Pull job list**](docs/pull_job_list.md) | Retrieves all jobs from Digital Recruiters and sends them to an Hrflow.ai Board. |
+| [**Pull profile list**](docs/pull_profile_list.md) | Retrieves all profiles from Digital Recruiters and sends them to an Hrflow.ai Source. |
+| [**Push profile**](docs/push_profile.md) | Pushes a profile from Hrflow.ai to Digital Recruiters. |
+
+
+
+
+
+
+
+
+# π 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 [Digitalrecruiters](https://www.digitalrecruiters.com/) to learn more.
+- π» [Connector code](https://github.com/Riminder/hrflow-connectors/tree/master/src/hrflow_connectors/connectors/digitalrecruiters) on our Github.
+
+# π Special Thanks
+
+- π» HrFlow.ai : Abdellahi Mezid - Software Engineer
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/__init__.py b/src/hrflow_connectors/connectors/digitalrecruiters/__init__.py
new file mode 100644
index 000000000..1f6b9750b
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/__init__.py
@@ -0,0 +1,3 @@
+from hrflow_connectors.connectors.digitalrecruiters.connector import ( # noqa
+ DigitalRecruiters,
+)
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/connector.py b/src/hrflow_connectors/connectors/digitalrecruiters/connector.py
new file mode 100644
index 000000000..59721fa2e
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/connector.py
@@ -0,0 +1,367 @@
+import re
+import typing as t
+from datetime import datetime
+
+from hrflow_connectors.connectors.digitalrecruiters.warehouse import (
+ DigitalRecruitersJobWarehouse,
+ DigitalRecruitersReadProfilesWarehouse,
+ DigitalRecruitersWriteProfileWarehouse,
+)
+from hrflow_connectors.connectors.hrflow.warehouse import (
+ HrFlowJobWarehouse,
+ HrFlowProfileParsingWarehouse,
+ HrFlowProfileWarehouse,
+)
+from hrflow_connectors.core import (
+ ActionName,
+ ActionType,
+ BaseActionParameters,
+ Connector,
+ ConnectorAction,
+ ConnectorType,
+ WorkflowType,
+)
+
+
+def html_to_plain_text(html_text):
+ if html_text is None:
+ return None
+ # Remove HTML tags
+ plain_text = re.sub(r"<.*?>", "", html_text)
+
+ # Replace special characters with their plain text equivalents
+ plain_text = plain_text.replace(" ", " ")
+ plain_text = plain_text.replace("&", "&")
+ plain_text = plain_text.replace(""", '"')
+ plain_text = plain_text.replace("'", "'")
+ plain_text = plain_text.replace("<", "<")
+ plain_text = plain_text.replace(">", ">")
+
+ # Remove extra whitespace and newline characters
+ plain_text = re.sub(r"\s+", " ", plain_text).strip()
+
+ return plain_text
+
+
+def get_job_location(digital_recruiters_adress: t.Union[t.Dict, None]) -> t.Dict:
+ if not digital_recruiters_adress:
+ return dict(lat=None, lng=None, text="")
+
+ lat = digital_recruiters_adress.get("position", {}).get("lat", None)
+ lat = float(lat) if lat is not None else lat
+
+ lng = digital_recruiters_adress.get("position", {}).get("lon", None)
+ lng = float(lng) if lng is not None else lng
+ text = digital_recruiters_adress.get("formatted", None)
+
+ return dict(lat=lat, lng=lng, text=text)
+
+
+def get_sections(digital_recruiters_job: t.Dict) -> t.List[t.Dict]:
+ sections = []
+ if (
+ "description" not in digital_recruiters_job
+ or "profile" not in digital_recruiters_job
+ ):
+ return sections
+
+ for section_name in [
+ "description",
+ "profile",
+ ]:
+ section = digital_recruiters_job.get(section_name, None)
+ if section is not None:
+ sections.append(
+ dict(
+ name=section_name,
+ title=section_name,
+ description=html_to_plain_text(section),
+ )
+ )
+ return sections
+
+
+def get_tags(digital_recruiters_job: t.Dict) -> t.List[t.Dict]:
+ job = digital_recruiters_job
+ custom_field_mapping = {
+ "PossibilitΓ© de tΓ©lΓ©travail": "digitalrecruiters_possibilite_de_teletravail",
+ "Automatisation (HRFlow.ai)": "digitalrecruiters_automatisation_hrflow",
+ "Heures hebdomadaires": "digitalrecruiters_heures_hebdomadaires",
+ "Date envisagΓ©e de recrutement": (
+ "digitalrecruiters_date_enviseagee_de_recrutement"
+ ),
+ "Date de fin": "digitalrecruiters_date_de_fin",
+ "Motif de recrutement": "digitalrecruiters_motif_de_recrutement",
+ "Nom de la personne remplacΓ©e": (
+ "digitalrecruiters_nom_de_la_personne_remplacee"
+ ),
+ "Echelon": "digitalrecruiters_echelon",
+ "Filière": "digitalrecruiters_filiere",
+ "Horaires": "digitalrecruiters_horaires",
+ "Un candidat est dΓ©jΓ identifiΓ©": "digitalrecruiters_candidat_deja_identifie",
+ "Nom de ce candidat": "digitalrecruiters_nom_du_candidat",
+ }
+
+ tags = []
+
+ def add_tag(name, value):
+ if value is not None and value != "":
+ tags.append({"name": name, "value": value})
+
+ compensation = job.get("salary", {})
+ if compensation:
+ add_tag("digitalrecruiters_compensation_min", compensation.get("min", None))
+ add_tag("digitalrecruiters_compensation_max", compensation.get("max", None))
+ add_tag(
+ "digitalrecruiters_compensation_currency",
+ compensation.get("currency", None),
+ )
+
+ manager = job.get("entity", {}).get("manager", {})
+ if manager:
+ add_tag("digitalrecruiters_manager_firstName", manager.get("firstname", None))
+ add_tag("digitalrecruiters_manager_lastName", manager.get("lastname", None))
+ add_tag("digitalrecruiters_manager_position", manager.get("position", None))
+ add_tag("digitalrecruiters_manager_picture", manager.get("picture_url", None))
+
+ recruiter = job.get("referent_recruiter", {})
+ if recruiter:
+ add_tag("digitalrecruiters_recruiter_email", recruiter.get("email", None))
+ add_tag(
+ "digitalrecruiters_recruiter_phoneNumber",
+ recruiter.get("phoneNumber", None),
+ )
+ add_tag(
+ "digitalrecruiters_recruiter_picture", recruiter.get("picture_url", None)
+ )
+
+ hierarchy_list = digital_recruiters_job.get("hierarchy", [])
+ for item in hierarchy_list:
+ depth = item.get("depth", None)
+ column_name = item.get("column_name", None)
+ public_name = item.get("public_name", None)
+ add_tag(f"hierarchy_{depth}", f"{column_name}:{public_name}")
+
+ custom_fields = job.get("custom_fields", [])
+ if custom_fields:
+ for custom_field in custom_fields:
+ name = custom_field.get("name", None)
+ value = custom_field.get("value", None)
+ mapped_name = custom_field_mapping.get(name, None)
+ if mapped_name:
+ add_tag(mapped_name, value)
+
+ return tags
+
+
+def format_skills(skills_list):
+ formatted_skills = [
+ {"name": skill, "type": None, "value": None} for skill in skills_list
+ ]
+ return formatted_skills
+
+
+# format profile location retrieved from DigitlRecruiters
+def get_profile_location(Dr_location: t.Dict) -> t.Dict:
+ if not Dr_location:
+ return dict(text="", lat=None, lng=None)
+ street = Dr_location.get("street", "")
+ zip_code = Dr_location.get("zip", "")
+ city = Dr_location.get("city", "")
+ country = Dr_location.get("country", "")
+
+ parts = []
+
+ if street:
+ parts.append(street)
+
+ if city:
+ parts.append(city)
+
+ if zip_code:
+ parts.append(f"({zip_code})")
+
+ if country:
+ parts.append(country)
+
+ location_text = ", ".join(parts)
+ location_lat = Dr_location.get("latitude", None)
+ location_lng = Dr_location.get("longitude", None)
+ return dict(text=location_text, lat=location_lat, lng=location_lng)
+
+
+def normalize_link(link):
+ normalized_link = link.replace("\\", "")
+ return normalized_link
+
+
+def format_job(digital_recruiters_job: t.Dict) -> t.Dict:
+ picture = None
+ pictures = digital_recruiters_job.get("pictures", [])
+ if pictures:
+ picture = pictures[0].get("default", None)
+ job = dict(
+ name=digital_recruiters_job.get("title", None),
+ picture=picture,
+ reference=digital_recruiters_job.get("reference", None),
+ created_at=digital_recruiters_job.get("published_at", None),
+ location=get_job_location(digital_recruiters_job.get("address", {})),
+ sections=get_sections(digital_recruiters_job),
+ requirements=html_to_plain_text(digital_recruiters_job.get("profile", None)),
+ skills=format_skills(digital_recruiters_job.get("skills", [])),
+ tags=get_tags(digital_recruiters_job),
+ )
+ return job
+
+
+def format_dr_profile(dr_candidate: t.Dict) -> t.Dict:
+ full_name = (
+ f"{dr_candidate.get('firstName', None)} {dr_candidate.get('lastName', None)}"
+ )
+ resume = dr_candidate.get("cv", {}).get("url", None)
+ resume = normalize_link(resume)
+ avatar = dr_candidate.get("avatar", {}).get("url", None)
+ reference = dr_candidate.get("id")
+ created_at = dr_candidate.get("createdAt")
+ resume = dr_candidate.get("resume")
+ location = get_profile_location(dr_candidate.get("location", {}))
+ # add tags
+ tags = []
+
+ def add_tag(name, value):
+ if value is not None:
+ tags.append({"name": name, "value": value})
+
+ add_tag("digitalrecruiters_profile-email", dr_candidate.get("email", None))
+ add_tag(
+ "digitalrecruiters_profile-phoneNumber", dr_candidate.get("phoneNumber", None)
+ )
+ add_tag("digitalrecruiters_profile-fullName", full_name)
+ add_tag("digitalrecruiters_avatar", avatar)
+ add_tag("digitalrecruiters_profile-resume", resume)
+ add_tag("digitalrecruiters_profile-location", location.get("text", None))
+ add_tag(
+ "digitalrecruiters_education-level", dr_candidate.get("educationLevel", None)
+ )
+ add_tag(
+ "digitalrecruiters_job-experience-level",
+ dr_candidate.get("experienceLevel", None),
+ )
+ add_tag("digitalrecruiters_job-title", dr_candidate.get("jobTitle", None))
+ add_tag("digitalrecruiters_job-id", dr_candidate.get("jobAd", {}).get("id", None))
+ add_tag(
+ "digitalrecruiters_job-published-at",
+ dr_candidate.get("jobAd", {}).get("publishedAt", None),
+ )
+ add_tag("digitalrecruiters_locale", dr_candidate.get("locale", None))
+ add_tag("digitalrecruiters_origin", dr_candidate.get("origin", None))
+ add_tag("digitalrecruiters_is-spontaneous", dr_candidate.get("isSpontaneous", None))
+ add_tag("digitalrecruiters_is-imported", dr_candidate.get("isImported", None))
+ add_tag(
+ "digitalrecruiters_is-from-external-api",
+ dr_candidate.get("isFromExternalApi", None),
+ )
+ add_tag(
+ "digitalrecruiters_rejected-reason", dr_candidate.get("rejectedReason", None)
+ )
+ add_tag(
+ "digitalrecruiters_application-status",
+ dr_candidate.get("applicationStatus", None),
+ )
+
+ metadatas = []
+ profile_hrflow = dict(
+ reference=reference,
+ created_at=created_at,
+ updated_at=datetime.utcnow().isoformat(),
+ resume=resume,
+ tags=tags,
+ metadatas=metadatas,
+ )
+ return profile_hrflow
+
+
+def format_profile(profile_hrflow: t.Dict) -> t.Dict:
+ dr_profile_dict = dict()
+
+ # Gender mapping: 1 for Male, 2 for Female
+ dr_profile_dict["gender"] = (
+ 1 if profile_hrflow["info"]["gender"].lower() == "male" else 2
+ )
+ dr_profile_dict["firstName"] = profile_hrflow["info"]["first_name"]
+ dr_profile_dict["lastName"] = profile_hrflow["info"]["last_name"]
+ dr_profile_dict["email"] = profile_hrflow["info"]["email"]
+ dr_profile_dict["phoneNumber"] = profile_hrflow["info"].get("phone")
+ location = profile_hrflow["info"].get("location", {})
+ dr_profile_dict["addressStreet"] = location.get("text")
+
+ fields = location.get("fields", {})
+ if isinstance(fields, list):
+ # If fields is a list, take the first element as it's a dictionary
+ fields = fields[0]
+ dr_profile_dict["addressZip"] = fields.get("postcode")
+ dr_profile_dict["addressCity"] = fields.get("state_district")
+
+ profile = dict()
+ profile["consent_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ profile["s_o"] = profile_hrflow.get("s_o", "")
+ profile["locale"] = "fr_FR"
+ profile["ApplicationProfile"] = dr_profile_dict
+
+ return profile
+
+
+DESCRIPTION = (
+ "Digital Recruiters: Tech-driven hiring platform with job posting,"
+ " automation, and analytics. Simplify recruitment, reduce time-to-hire,"
+ " and elevate candidate experience. Streamline the hiring process for"
+ " businesses with advanced features and integration capabilities."
+)
+
+DigitalRecruiters = Connector(
+ name="DigitalRecruiters",
+ type=ConnectorType.ATS,
+ description=DESCRIPTION,
+ url="https://www.digitalrecruiters.com/",
+ actions=[
+ ConnectorAction(
+ name=ActionName.pull_job_list,
+ trigger_type=WorkflowType.pull,
+ description=(
+ "Retrieves all jobs from Digital Recruiters and sends them to an"
+ " Hrflow.ai Board."
+ ),
+ parameters=BaseActionParameters.with_defaults(
+ "ReadJobsActionParameters", format=format_job
+ ),
+ origin=DigitalRecruitersJobWarehouse,
+ target=HrFlowJobWarehouse,
+ action_type=ActionType.inbound,
+ ),
+ ConnectorAction(
+ name=ActionName.pull_profile_list,
+ trigger_type=WorkflowType.pull,
+ description=(
+ "Retrieves all profiles from Digital Recruiters and sends them to an"
+ " Hrflow.ai Source."
+ ),
+ parameters=BaseActionParameters.with_defaults(
+ "ReadProfilesActionParameters", format=format_dr_profile
+ ),
+ origin=DigitalRecruitersReadProfilesWarehouse,
+ target=HrFlowProfileParsingWarehouse,
+ action_type=ActionType.inbound,
+ ),
+ ConnectorAction(
+ name=ActionName.push_profile,
+ trigger_type=WorkflowType.catch,
+ description="Pushes a profile from Hrflow.ai to Digital Recruiters.",
+ parameters=BaseActionParameters.with_defaults(
+ "WriteProfilesActionParameters", format=format_profile
+ ),
+ origin=HrFlowProfileWarehouse,
+ target=DigitalRecruitersWriteProfileWarehouse,
+ action_type=ActionType.outbound,
+ ),
+ ],
+)
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/docs/pull_job_list.md b/src/hrflow_connectors/connectors/digitalrecruiters/docs/pull_job_list.md
new file mode 100644
index 000000000..1588dcbe1
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/docs/pull_job_list.md
@@ -0,0 +1,73 @@
+# Pull job list
+`DigitalRecruiters Jobs` :arrow_right: `HrFlow.ai Jobs`
+
+Retrieves all jobs from Digital Recruiters and sends them to an Hrflow.ai Board.
+
+
+**DigitalRecruiters Jobs endpoints used :**
+| Endpoints | Description |
+| --------- | ----------- |
+| [**Read Jobs**]({url_environnement}/export/job-ads/{token}) | Read jobs from Digital Recruiters |
+
+
+
+## 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_job`](../connector.py#L198) | 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 |
+| ----- | ---- | ------- | ----------- |
+| `token` :red_circle: | `str` | None | Digital Recruiters API token. |
+| `environment_url` :red_circle: | `str` | None | Digital Recruiters API url environnement. |
+
+## 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 DigitalRecruiters
+from hrflow_connectors.core import ReadMode
+
+
+logging.basicConfig(level=logging.INFO)
+
+
+DigitalRecruiters.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(
+ token="your_token",
+ environment_url="your_environment_url",
+ ),
+ 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/connectors/digitalrecruiters/docs/pull_profile_list.md b/src/hrflow_connectors/connectors/digitalrecruiters/docs/pull_profile_list.md
new file mode 100644
index 000000000..d0338a6e5
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/docs/pull_profile_list.md
@@ -0,0 +1,81 @@
+# Pull profile list
+`DigitalRecruiters Read Profils` :arrow_right: `HrFlow.ai Profile Parsing`
+
+Retrieves all profiles from Digital Recruiters and sends them to an Hrflow.ai Source.
+
+
+**DigitalRecruiters Read Profils endpoints used :**
+| Endpoints | Description |
+| --------- | ----------- |
+| [**Read Profiles**]({url_environnement}/public/v1/{endpoint}) | Read profiles from Digital Recruiters |
+
+
+
+## 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_dr_profile`](../connector.py#L217) | 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_key` :red_circle: | `str` | None | DigitalRecruiters API key |
+| `username` :red_circle: | `str` | None | Username for authentication |
+| `password` :red_circle: | `str` | None | Password for authentication |
+| `environment_url` :red_circle: | `` | None | URL environment for the API |
+| `jobAd` | `int` | None | Optional: Id of a job advertisement |
+| `sort` | `str` | None | Optional: Field to sort by (id, firstName, lastName, createdAt, updatedAt) |
+| `limit` | `int` | 50 | Optional: Limit the number of results returned |
+| `page` | `int` | 1 | Optional: Page number of results returned |
+
+## 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 |
+| `only_insert` | `bool` | False | When enabled the profile is written only if it doesn't exist in the source |
+
+:red_circle: : *required*
+
+## Example
+
+```python
+import logging
+from hrflow_connectors import DigitalRecruiters
+from hrflow_connectors.core import ReadMode
+
+
+logging.basicConfig(level=logging.INFO)
+
+
+DigitalRecruiters.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(
+ api_key="your_api_key",
+ username="your_username",
+ password="your_password",
+ environment_url=***,
+ jobAd=0,
+ sort="your_sort",
+ limit=50,
+ page=1,
+ ),
+ target_parameters=dict(
+ api_secret="your_api_secret",
+ api_user="your_api_user",
+ source_key="your_source_key",
+ only_insert=False,
+ )
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/docs/push_profile.md b/src/hrflow_connectors/connectors/digitalrecruiters/docs/push_profile.md
new file mode 100644
index 000000000..b34d1621d
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/docs/push_profile.md
@@ -0,0 +1,73 @@
+# Push profile
+`HrFlow.ai Profiles` :arrow_right: `DigitalRecruiters Write Profile`
+
+Pushes a profile from Hrflow.ai to Digital Recruiters.
+
+
+
+**DigitalRecruiters Write Profile endpoints used :**
+| Endpoints | Description |
+| --------- | ----------- |
+| [**Write Profile**]({url_environnement}/api/candidate/apply/{token}) | Write profile to Digital Recruiters |
+
+
+## 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#L284) | 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 |
+| ----- | ---- | ------- | ----------- |
+| `token` :red_circle: | `str` | None | Digital Recruiters API token. |
+| `environment_url` :red_circle: | `str` | None | Digital Recruiters API url environnement. |
+| `job_reference` :red_circle: | `str` | None | reference of the job to which the candidate is applying. |
+| `message` | `str` | message du candidat | Application message. |
+
+:red_circle: : *required*
+
+## Example
+
+```python
+import logging
+from hrflow_connectors import DigitalRecruiters
+from hrflow_connectors.core import ReadMode
+
+
+logging.basicConfig(level=logging.INFO)
+
+
+DigitalRecruiters.push_profile(
+ 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(
+ token="your_token",
+ environment_url="your_environment_url",
+ job_reference="your_job_reference",
+ message="message du candidat",
+ )
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/logo.png b/src/hrflow_connectors/connectors/digitalrecruiters/logo.png
new file mode 100644
index 000000000..4c8ed5cc8
Binary files /dev/null and b/src/hrflow_connectors/connectors/digitalrecruiters/logo.png differ
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/schema.py b/src/hrflow_connectors/connectors/digitalrecruiters/schema.py
new file mode 100644
index 000000000..edfaa5980
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/schema.py
@@ -0,0 +1,192 @@
+import typing as t
+from datetime import datetime
+
+from pydantic import BaseModel, HttpUrl
+
+
+class ContractDuration(BaseModel):
+ min: t.Optional[int]
+ max: t.Optional[int]
+
+
+class Salary(BaseModel):
+ min: t.Optional[int]
+ max: t.Optional[int]
+ kind: t.Optional[str]
+ rate_type: t.Optional[str]
+ variable: t.Optional[str]
+ currency: t.Optional[str]
+
+
+class AddressParts(BaseModel):
+ street: str
+ zip: str
+ city: str
+ county: str
+ state: str
+ country: str
+
+
+class Address(BaseModel):
+ parts: AddressParts
+ formatted: str
+ position: t.Dict[str, str]
+
+
+class Manager(BaseModel):
+ section_title: str
+ section_body: str
+ picture_url: t.Optional[str]
+ firstname: str
+ lastname: str
+ position: str
+
+
+class Hierarchy(BaseModel):
+ depth: int
+ column_name: str
+ public_name: str
+
+
+class Entity(BaseModel):
+ public_name: str
+ internal_ref: str
+ around: str
+ address: Address
+ manager: Manager
+ hierarchy: t.List[Hierarchy]
+
+
+class ReferentRecruiter(BaseModel):
+ firstname: str
+ lastname: str
+ picture_url: t.Optional[str]
+
+
+class Brand(BaseModel):
+ name: str
+ description: str
+ logo: str
+ favicon: str
+
+
+class CustomField(BaseModel):
+ hash: str
+ name: str
+ value: str
+
+
+class DigitalRecruitersJob(BaseModel):
+ locale: str
+ reference: str
+ published_at: str
+ catch_phrase: str
+ contract_type: str
+ contract_duration: ContractDuration
+ contract_work_period: str
+ service: str
+ experience_level: str
+ education_level: str
+ title: str
+ description: str
+ profile: str
+ skills: t.List[str]
+ salary: Salary
+ pictures: t.List[str]
+ videos: t.List[str]
+ internal_apply_url: t.Optional[str]
+ apply_url: t.Optional[str]
+ address: Address
+ entity: Entity
+ referent_recruiter: ReferentRecruiter
+ brand: Brand
+ custom_fields: t.List[CustomField]
+ count_recruited: t.Optional[str]
+
+
+class DigitalRecruitersCandidateProfile(BaseModel):
+ gender: int
+ firstName: str
+ lastName: str
+ email: str
+ phoneNumber: t.Optional[str] = None
+ job: t.Optional[str] = None
+ addressStreet: t.Optional[str] = None
+ addressZip: t.Optional[str] = None
+ addressCity: t.Optional[str] = None
+
+
+class DigitalRecruitersImportCandidateMessage(BaseModel):
+ message: str
+
+
+class DigitalRecruitersImportCandidateFile(BaseModel):
+ content: str
+ name: str
+
+
+class DigitalRecruitersWriteProfile(BaseModel):
+ reference: str
+ consent_date: str
+ s_o: str
+ locale: str
+ ApplicationMessage: DigitalRecruitersImportCandidateMessage
+ ApplicationProfile: DigitalRecruitersCandidateProfile
+ file: t.Optional[DigitalRecruitersImportCandidateFile] = None
+
+
+class Location(BaseModel):
+ zip: str
+ city: str
+ county: t.Optional[str]
+ state: t.Optional[str]
+ country: str
+ latitude: t.Optional[float]
+ longitude: t.Optional[float]
+
+
+class ContractItem(BaseModel):
+ id: int
+ name: str
+ countryNodeIds: t.Optional[t.List[int]]
+
+
+class JobReference(BaseModel):
+ label: str
+ hashId: str
+
+
+class Privacy(BaseModel):
+ status: str
+ updatedAt: t.Optional[datetime]
+
+
+class Avatar(BaseModel):
+ url: HttpUrl
+
+
+class CV(BaseModel):
+ url: HttpUrl
+
+
+class Resume(BaseModel):
+ raw: bytes
+ content_type: str
+
+
+class DigitalRecruitersReadProfile(BaseModel):
+ id: int
+ firstName: str
+ lastName: str
+ createdAt: datetime
+ jobTitle: str
+ avatar: Avatar
+ gender: str
+ email: str
+ location: Location
+ contract: ContractItem
+ status: str
+ jobReference: JobReference
+ privacy: Privacy
+ cv: CV
+ resume: t.Optional[Resume]
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/test-config.yaml b/src/hrflow_connectors/connectors/digitalrecruiters/test-config.yaml
new file mode 100644
index 000000000..bebcc7e8e
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/test-config.yaml
@@ -0,0 +1,68 @@
+warehouse:
+ DigitalRecruitersJobWarehouse:
+ read:
+ - parameters:
+ token: $__TOKEN_SUCCESS
+ environment_url: $__ENVIRONMENT_URL_SUCCESS
+actions:
+ pull_job_list:
+ - id: valid_parameters
+ origin_parameters:
+ token: $__TOKEN_SUCCESS
+ environment_url: $__ENVIRONMENT_URL_SUCCESS
+ target_parameters:
+ api_secret: $__API_SECRET_SUCCESS
+ api_user: $__API_USER_SUCCESS
+ board_key: $__BOARD_KEY_SUCCESS
+ status: success
+ - id: invalid_parameters
+ origin_parameters:
+ token: $__TOKEN_SUCCESS
+ environment_url: $__ENVIRONMENT_URL_FAILURE
+ target_parameters:
+ api_secret: $__API_SECRET_SUCCESS
+ api_user: $__API_USER_SUCCESS
+ board_key: $__BOARD_KEY_SUCCESS
+ status: fatal
+ reason: read_failure
+ - id: invalid_token
+ origin_parameters:
+ token: $__TOKEN_FAILURE
+ environment_url: $__ENVIRONMENT_URL_SUCCESS
+ target_parameters:
+ api_secret: $__API_SECRET_SUCCESS
+ api_user: $__API_USER_SUCCESS
+ board_key: $__BOARD_KEY_SUCCESS
+ status: fatal
+ reason: read_failure
+ - id: invalid_api_secret
+ origin_parameters:
+ token: $__TOKEN_SUCCESS
+ environment_url: $__ENVIRONMENT_URL_SUCCESS
+ target_parameters:
+ api_secret: $__API_SECRET_FAILURE
+ api_user: $__API_USER_SUCCESS
+ board_key: $__BOARD_KEY_SUCCESS
+ status: fatal
+ reason: write_failure
+ - id: invalid_api_user
+ origin_parameters:
+ token: $__TOKEN_SUCCESS
+ environment_url: $__ENVIRONMENT_URL_SUCCESS
+ target_parameters:
+ api_secret: $__API_SECRET_SUCCESS
+ api_user: $__API_USER_FAILURE
+ board_key: $__BOARD_KEY_SUCCESS
+ status: fatal
+ reason: write_failure
+ - id: invalid_board_key
+ origin_parameters:
+ token: $__TOKEN_SUCCESS
+ environment_url: $__ENVIRONMENT_URL_SUCCESS
+ target_parameters:
+ api_secret: $__API_SECRET_SUCCESS
+ api_user: $__API_USER_SUCCESS
+ board_key: $__BOARD_KEY_FAILURE
+ status: fatal
+ reason: write_failure
+
diff --git a/src/hrflow_connectors/connectors/digitalrecruiters/warehouse.py b/src/hrflow_connectors/connectors/digitalrecruiters/warehouse.py
new file mode 100644
index 000000000..4275c8929
--- /dev/null
+++ b/src/hrflow_connectors/connectors/digitalrecruiters/warehouse.py
@@ -0,0 +1,343 @@
+import typing as t
+from logging import LoggerAdapter
+
+import requests
+from pydantic import Field, HttpUrl
+
+from hrflow_connectors.connectors.digitalrecruiters.schema import (
+ DigitalRecruitersJob,
+ DigitalRecruitersReadProfile,
+ DigitalRecruitersWriteProfile,
+)
+from hrflow_connectors.core import (
+ ActionEndpoints,
+ DataType,
+ FieldType,
+ ParametersModel,
+ ReadMode,
+ Warehouse,
+ WarehouseReadAction,
+ WarehouseWriteAction,
+)
+
+DIGITAL_RECRUITERS_JOBS_ENDPOINT = "{url_environnement}/export/job-ads/{token}"
+DIGITAL_RECRUITERS_WRITE_PROFILES_ENDPOINT = (
+ "{url_environnement}/api/candidate/apply/{token}"
+)
+DIGITAL_RECRUITERS_READ_PROFILES_ENDPOINT = "{url_environnement}/public/v1/{endpoint}"
+
+
+class ReadJobsParameters(ParametersModel):
+ token: str = Field(
+ ...,
+ description="Digital Recruiters API token.",
+ repr=False,
+ field_type=FieldType.Auth,
+ )
+ environment_url: str = Field(
+ ...,
+ description="Digital Recruiters API url environnement.",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+
+
+class ReadProfileParameters(ParametersModel):
+ api_key: str = Field(
+ ...,
+ description="DigitalRecruiters API key",
+ repr=False,
+ field_type=FieldType.Auth,
+ )
+ username: str = Field(
+ ...,
+ description="Username for authentication",
+ repr=False,
+ field_type=FieldType.Auth,
+ )
+ password: str = Field(
+ ...,
+ description="Password for authentication",
+ repr=False,
+ field_type=FieldType.Auth,
+ )
+ environment_url: HttpUrl = Field(
+ ...,
+ description="URL environment for the API",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+ jobAd: int = Field(
+ None,
+ description="Optional: Id of a job advertisement",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+ sort: str = Field(
+ None,
+ description=(
+ "Optional: Field to sort by (id, firstName, lastName, createdAt, updatedAt)"
+ ),
+ repr=False,
+ field_type=FieldType.Other,
+ )
+ limit: int = Field(
+ 50,
+ description="Optional: Limit the number of results returned",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+ page: int = Field(
+ 1,
+ description="Optional: Page number of results returned",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+
+
+class WriteProfilesParameters(ParametersModel):
+ token: str = Field(
+ ...,
+ description="Digital Recruiters API token.",
+ repr=False,
+ field_type=FieldType.Auth,
+ )
+ environment_url: str = Field(
+ ...,
+ description="Digital Recruiters API url environnement.",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+ job_reference: str = Field(
+ ...,
+ description="reference of the job to which the candidate is applying.",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+ message: t.Optional[str] = Field(
+ "message du candidat",
+ description="Application message.",
+ repr=False,
+ field_type=FieldType.Other,
+ )
+
+
+class TokenExpired(Exception):
+ pass
+
+
+def get_initial_token(params: ReadProfileParameters):
+ url = f"{params.environment_url}/public/v1/users/login"
+ headers = {"Content-Type": "application/json", "X-DR-API-KEY": params.api_key}
+ payload = {"username": params.username, "password": params.password}
+
+ response = requests.post(url, headers=headers, json=payload)
+
+ if response.status_code == 200:
+ return response.json()["token"], response.json()["refresh_token"]
+ raise Exception(
+ "Failed to get token from Digitial Recruiters API with"
+ f" status_code={response.status_code} and message={response.text}"
+ )
+
+
+def get_refresh_token(params: ReadProfileParameters, refresh_token: str):
+ url = f"{params.environment_url}/public/v1/users/refresh"
+
+ headers = {"Content-Type": "application/json", "X-DR-API-KEY": params.api_key}
+
+ payload = {"token": refresh_token}
+
+ response = requests.post(url, headers=headers, json=payload)
+
+ if response.status_code == 200:
+ return response.json()["token"], response.json()["refresh_token"]
+ raise Exception(
+ "Failed to refresh token from Digitial Recruiters API with"
+ f" status_code={response.status_code} and message={response.text}"
+ )
+
+
+def get_candidates(url, params: ReadProfileParameters, token):
+ headers = {
+ "Content-Type": "application/json",
+ "X-DR-API-KEY": params.api_key,
+ "Authorization": f"Bearer {token}",
+ }
+
+ params_dict = dict(
+ jobAd=params.jobAd, sort=params.sort, limit=params.limit, page=params.page
+ )
+ response = requests.get(url, headers=headers, params=params_dict)
+
+ if response.status_code == 200:
+ data = response.json()["data"]
+ return data.get("items", []), data.get("links", {}).get("next")
+ elif response.status_code == 401:
+ raise TokenExpired(f"Token expired. Error: {response.text}")
+ raise Exception(
+ "Failed to get candidates from Digitial Recruiters API with"
+ f" status_code={response.status_code} and message={response.text}"
+ )
+
+
+def fetch_and_add_resume(candidate, token):
+ cv_url = candidate.get("CV", {}).get("url")
+ if cv_url:
+ headers = {"Authorization": f"Bearer {token}"}
+ response = requests.get(cv_url, headers=headers)
+
+ if response.status_code == 200:
+ resume_info = {
+ "raw": response.content,
+ "content_type": response.headers.get("Content-Type"),
+ }
+ candidate["resume"] = resume_info
+ elif response.status_code == 401:
+ raise TokenExpired(f"Token expired. Error: {response.text}")
+ else:
+ raise Exception(
+ "Failed to get resume from Digitial Recruiters API with"
+ f" status_code={response.status_code} and message={response.text}"
+ )
+ return candidate
+
+
+def read_jobs(
+ adapter: LoggerAdapter,
+ parameters: ReadJobsParameters,
+ read_mode: t.Optional[ReadMode] = None,
+ read_from: t.Optional[str] = None,
+) -> t.Iterable[t.Dict]:
+ DR_JOB_URL = parameters.environment_url.rstrip("/")
+ url = DIGITAL_RECRUITERS_JOBS_ENDPOINT.format(
+ url_environnement=DR_JOB_URL, token=parameters.token
+ )
+ response = requests.get(url)
+ if response.status_code != 200:
+ raise Exception(
+ "Failed to get jobs from Digital Recruiters API with"
+ f" status_code={response.status_code} and message={response.text}"
+ )
+ jobs = response.json().get("ads", [])
+ adapter.info(f"Found {len(jobs)} jobs in Digital Recruiters.")
+ if not jobs:
+ adapter.warning("No jobs found in Digital Recruiters.")
+ return
+ for job in jobs:
+ yield job
+
+
+def read_profiles(
+ adapter: LoggerAdapter,
+ params: ReadProfileParameters,
+ read_mode: t.Optional[ReadMode] = None,
+ read_from: t.Optional[str] = None,
+) -> t.Iterable[t.Dict]:
+ token, refresh_token = get_initial_token(params)
+ next_page_link = f"{params.environment_url}/job-applications/detailed"
+ while next_page_link:
+ try:
+ candidates, next_page_link = get_candidates(next_page_link, params, token)
+ except TokenExpired:
+ token, refresh_token = get_refresh_token(params, refresh_token)
+ candidates, next_page_link = get_candidates(next_page_link, params, token)
+
+ if not candidates:
+ break
+ for candidate in candidates:
+ try:
+ candidate = fetch_and_add_resume(candidate, token)
+ except TokenExpired:
+ token, refresh_token = get_refresh_token(params, refresh_token)
+ candidate = fetch_and_add_resume(candidate, token)
+ yield candidate
+
+
+def write(
+ adapter: LoggerAdapter,
+ parameters: WriteProfilesParameters,
+ profiles: t.Iterable[t.Dict],
+) -> t.List[t.Dict]:
+ DR_PROFILES_URL = parameters.environment_url.rstrip("/")
+ url = DIGITAL_RECRUITERS_WRITE_PROFILES_ENDPOINT.format(
+ url_environnement=DR_PROFILES_URL, token=parameters.token
+ )
+
+ failed_profiles = []
+
+ for profile in profiles:
+ profile["reference"] = parameters.job_reference
+ profile["ApplicationMessage"] = dict(
+ message=parameters.message # Candidate Message
+ )
+ response = requests.post(url, json=profile)
+
+ if response.status_code == 201:
+ adapter.info("Candidate profile pushed successfully.")
+ elif response.status_code == 202:
+ adapter.warning(
+ "Candidate profile pushed, but some information is missing."
+ )
+ else:
+ adapter.error(
+ "Failed to push candidate profile. Status code: %s, Response: %s",
+ response.status_code,
+ response.text,
+ )
+ failed_profiles.append(profile)
+
+ return failed_profiles
+
+
+# Define the Warehouse for Digital Recruiters jobs and profiles
+DigitalRecruitersJobWarehouse = Warehouse(
+ name="DigitalRecruiters Jobs",
+ data_schema=DigitalRecruitersJob,
+ data_type=DataType.job,
+ read=WarehouseReadAction(
+ parameters=ReadJobsParameters,
+ function=read_jobs,
+ endpoints=[
+ ActionEndpoints(
+ name="Read Jobs",
+ description="Read jobs from Digital Recruiters",
+ url=DIGITAL_RECRUITERS_JOBS_ENDPOINT,
+ )
+ ],
+ ),
+)
+
+# Define the Warehouse for Digital Recruiters jobs and profiles
+DigitalRecruitersReadProfilesWarehouse = Warehouse(
+ name="DigitalRecruiters Read Profils",
+ data_schema=DigitalRecruitersReadProfile,
+ data_type=DataType.profile,
+ read=WarehouseReadAction(
+ parameters=ReadProfileParameters,
+ function=read_profiles,
+ endpoints=[
+ ActionEndpoints(
+ name="Read Profiles",
+ description="Read profiles from Digital Recruiters",
+ url=DIGITAL_RECRUITERS_READ_PROFILES_ENDPOINT,
+ )
+ ],
+ ),
+)
+DigitalRecruitersWriteProfileWarehouse = Warehouse(
+ name="DigitalRecruiters Write Profile",
+ data_schema=DigitalRecruitersWriteProfile,
+ data_type=DataType.profile,
+ write=WarehouseWriteAction(
+ parameters=WriteProfilesParameters,
+ function=write,
+ endpoints=[
+ ActionEndpoints(
+ name="Write Profile",
+ description="Write profile to Digital Recruiters",
+ url=DIGITAL_RECRUITERS_WRITE_PROFILES_ENDPOINT,
+ )
+ ],
+ ),
+)