Repository: Riminder/hrflow-connectors (language: python)

Issue:
Feature: SmartRecruiters connector to pull jobs into HrFlow.ai boards
This merge request will add in the hrflow_connectors boards a new connector for SmartRecruiters job postings:

> actions.py which containts the base scripts to run and send a request to SmartRecruiters API to pull their job postings

> The file contains the `SmartJobs` class, which inherits the `BoardAction` class, divided into the main functions `pull` that pull jobs data and `format` that formats the jobs into HrFlow job object format.

--It will also add inside the tests folder, a file test_smart_jobs.py to test the connector.






Discussion / hints:

Fix patch:
diff --git a/src/hrflow_connectors/__init__.py b/src/hrflow_connectors/__init__.py
index 1da36f835..9c0edeab8 100644
--- a/src/hrflow_connectors/__init__.py
+++ b/src/hrflow_connectors/__init__.py
@@ -1,3 +1,4 @@
+from hrflow_connectors.connectors.ceridian import Ceridian
 from hrflow_connectors.connectors.bullhorn import Bullhorn
 from hrflow_connectors.connectors.sapsuccessfactors import SAPSuccessFactors
 from hrflow_connectors.connectors.breezyhr import BreezyHR
@@ -12,7 +13,7 @@
 from hrflow_connectors.core.documentation import generate_docs  # noqa
 
 __version__ = "2.0.0"
-__CONNECTORS__ = [SmartRecruiters, TalentSoft, PoleEmploi, Adzuna, Recruitee, Workable, BreezyHR, SAPSuccessFactors, Bullhorn]
+__CONNECTORS__ = [SmartRecruiters, TalentSoft, PoleEmploi, Adzuna, Recruitee, Workable, BreezyHR, SAPSuccessFactors, Bullhorn, Ceridian]
 
 # This makes sure that connector are in module namespace
 # and that the automatic workflow code generation should work
diff --git a/src/hrflow_connectors/connectors/ceridian/DOCUMENTATION.md b/src/hrflow_connectors/connectors/ceridian/DOCUMENTATION.md
new file mode 100644
index 000000000..13a48bb65
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/DOCUMENTATION.md
@@ -0,0 +1,18 @@
+
+
+
+# Ceridian Connector
+
+## About Ceridian:
+
+Cloud HCM software that brings together real-time payroll, HR, benefits, time reporting, talent and workforce management to empower your people.
+
+## Connector features:
+
+-   **Pull jobs** The connector retrieves job information from the Ceridian website using the Ceridian API and indexes it to an HrFlow Board.
+
+## Useful links:
+
+📄Visit [Ceridian](https://www.workable.com/) to learn more.
+
+💻 [Connector code]([https://github.com/Sprenger07/hrflow-connectors/tree/feature/update-connector-workable/src/hrflow_connectors/connectors/workable](https://github.com/Sprenger07/hrflow-connectors/tree/feature/update-connector-workable/src/hrflow_connectors/connectors/workable)) on our Github.
diff --git a/src/hrflow_connectors/connectors/ceridian/README.md b/src/hrflow_connectors/connectors/ceridian/README.md
new file mode 100644
index 000000000..c3b34c04a
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/README.md
@@ -0,0 +1,11 @@
+
+# Ceridian Connector
+
+> Dayforce enterprise HCM software combines payroll, HR, benefits, talent and workforce management in a single cloud application to power the future of work.Ceridian
+
+
+🔗 https://www.ceridian.com/
+
+| Actions |
+| ------- |
+| [**Pull jobs**](docs/pull_jobs.md) |
diff --git a/src/hrflow_connectors/connectors/ceridian/__init__.py b/src/hrflow_connectors/connectors/ceridian/__init__.py
new file mode 100644
index 000000000..0c5f6bebe
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/__init__.py
@@ -0,0 +1,1 @@
+from hrflow_connectors.connectors.ceridian.connector import Ceridian
diff --git a/src/hrflow_connectors/connectors/ceridian/connector.py b/src/hrflow_connectors/connectors/ceridian/connector.py
new file mode 100644
index 000000000..93431792b
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/connector.py
@@ -0,0 +1,96 @@
+import typing as t
+
+from hrflow_connectors.connectors.ceridian.warehouse import CeridianJobWarehouse
+from hrflow_connectors.connectors.hrflow.warehouse import HrFlowJobWarehouse
+from hrflow_connectors.core import (
+    BaseActionParameters,
+    Connector,
+    ConnectorAction,
+    WorkflowType,
+)
+
+from .schemas import CeridianDayforceJobModel
+
+
+def format_job(data: CeridianDayforceJobModel) -> t.Dict:
+    """
+    format a job into the hrflow job object format
+    Args:
+        data (CeridianDayforceJobModel): a job object pulled from a ceridian
+        dayforce job feed space
+    Returns:
+        HrflowJob: a job into the hrflow job object format
+    """
+    job = dict()
+
+    # basic information
+    job["name"] = data.get("Title")
+    job["summary"] = None
+    job["reference"] = str(data.get("ReferenceNumber")) + str(
+        data.get("ParentRequisitionCode")
+    )
+    job["url"] = data.get("JobDetailsUrl")
+
+    # location
+    location = data.get("City")
+    state = data.get("State")
+    country = data.get("Country")
+    postal_code = data.get("PostalCode")
+    geojson = dict(state=state, country=country, postal_code=postal_code)
+    job["location"] = dict(text=location, lat=None, lng=None, geojson=geojson)
+
+    # sections
+    description = data.get("Description")
+    job["sections"] = [
+        dict(
+            name="dayforce_description",
+            title="dayforce_description",
+            description=description,
+        )
+    ]
+    job["created_at"] = data.get("DatePosted")
+    job["updated_at"] = data.get("LastUpdated")
+
+    # tags
+    apply_url = str(data.get("ApplyUrl"))
+    client_site_name = str(data.get("ClientSiteName"))
+    client_site_ref_code = str(data.get("ClientSiteXRefCode"))
+    company_name = str(data.get("CompanyName"))
+    remote = str(data.get("IsVirtualLocation"))
+    job["tags"] = [
+        dict(name="dayforce_apply_url", value=apply_url),
+        dict(name="dayforce_client-site-name", value=client_site_name),
+        dict(name="dayforce_client-site-ref-code", value=client_site_ref_code),
+        dict(name="dayforce_company_name", value=company_name),
+        dict(name="dayforce_remote", value=remote),
+    ]
+
+    return job
+
+
+DESCRIPTION = (
+    "Dayforce enterprise HCM software combines payroll, HR, benefits, talent and"
+    " workforce management in a single cloud application to power the future of"
+    " work.Ceridian"
+)
+
+Ceridian = Connector(
+    name="Ceridian",
+    description=DESCRIPTION,
+    url="https://www.ceridian.com/",
+    actions=[
+        ConnectorAction(
+            name="pull_jobs",
+            trigger_type=WorkflowType.pull,
+            description=(
+                "Retrieve all jobs via the ***Ceridian*** API and send them"
+                " to an ***Hrflow.ai Board***."
+            ),
+            parameters=BaseActionParameters.with_defaults(
+                "ReadJobsActionParameters", format=format_job
+            ),
+            origin=CeridianJobWarehouse,
+            target=HrFlowJobWarehouse,
+        ),
+    ],
+)
diff --git a/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md b/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
new file mode 100644
index 000000000..e1e82438e
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
@@ -0,0 +1,76 @@
+
+# Pull jobs
+`Ceridian Jobs` :arrow_right: `HrFlow.ai Jobs`
+
+Retrieve all jobs via the ***Ceridian*** API and send them to an ***Hrflow.ai Board***.
+
+
+
+## Action Parameters
+
+| Field | Type | Default | Description |
+| ----- | ---- | ------- | ----------- |
+| `logics`  | `typing.List[typing.Callable[[typing.Dict], typing.Optional[typing.Dict]]]` | [] | List of logic functions |
+| `format`  | `typing.Callable[[typing.Dict], typing.Dict]` | [`format_job`](../connector.py#L15) | 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 |
+| ----- | ---- | ------- | ----------- |
+| `subdomain` :red_circle: | `str` | None | Subdomain used to access Ceridian API |
+| `client_name_space` :red_circle: | `str` | None | Client name space used to access Ceridian API |
+| `companyName`  | `str` | None | Company name. Example: XYZ Co. |
+| `parentCompanyName`  | `str` | None | Parent Company name. Example: Ceridian |
+| `lastUpdateTimeFrom`  | `str` | None | A starting timestamp of job posting date. Example: 2017-01-01T13:24:56 |
+| `htmlDescription`  | `bool` | None | A flag to feed the jobs over with html formatting or plain text description |
+
+## 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 Ceridian
+from hrflow_connectors.core import ReadMode
+
+
+logging.basicConfig(level=logging.INFO)
+
+
+Ceridian.pull_jobs(
+    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(
+        subdomain="your_subdomain",
+        client_name_space="your_client_name_space",
+        companyName="your_companyName",
+        parentCompanyName="your_parentCompanyName",
+        lastUpdateTimeFrom="your_lastUpdateTimeFrom",
+        htmlDescription=False,
+    ),
+    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/ceridian/schemas.py b/src/hrflow_connectors/connectors/ceridian/schemas.py
new file mode 100644
index 000000000..2e18bfc16
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/schemas.py
@@ -0,0 +1,24 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class CeridianDayforceJobModel(BaseModel):
+    Title: str
+    Description: Optional[str]
+    ClientSiteName: Optional[str]
+    ClientSiteXRefCode: Optional[str]
+    CompanyName: Optional[str]
+    ParentCompanyName: Optional[str]
+    JobDetailsUrl: str
+    ApplyUrl: Optional[str]
+    AddressLine1: Optional[str]
+    City: Optional[str]
+    State: Optional[str]
+    Country: Optional[str]
+    PostalCode: Optional[str]
+    DatePosted: Optional[str]
+    LastUpdated: Optional[str]
+    ReferenceNumber: int
+    ParentRequisitionCode: Optional[int]
+    IsVirtualLocation: Optional[bool]
diff --git a/src/hrflow_connectors/connectors/ceridian/warehouse.py b/src/hrflow_connectors/connectors/ceridian/warehouse.py
new file mode 100644
index 000000000..ce0d7d640
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/warehouse.py
@@ -0,0 +1,107 @@
+import typing as t
+from logging import LoggerAdapter
+
+import requests
+from pydantic import Field
+
+from hrflow_connectors.connectors.ceridian.schemas import CeridianDayforceJobModel
+from hrflow_connectors.core import (
+    DataType,
+    FieldType,
+    ParametersModel,
+    ReadMode,
+    Warehouse,
+    WarehouseReadAction,
+)
+
+
+class ReadJobsParameters(ParametersModel):
+    subdomain: str = Field(
+        ...,
+        description="Subdomain used to access Ceridian API",
+        repr=False,
+        field_type=FieldType.Other,
+    )
+
+    client_name_space: str = Field(
+        ...,
+        description="Client name space used to access Ceridian API",
+        repr=False,
+        field_type=FieldType.Other,
+    )
+
+    companyName: t.Optional[str] = Field(
+        None,
+        description="Company name. Example: XYZ Co.",
+        field_type=FieldType.QueryParam,
+    )
+    parentCompanyName: t.Optional[str] = Field(
+        None,
+        description="Parent Company name. Example: Ceridian",
+        field_type=FieldType.QueryParam,
+    )
+
+    lastUpdateTimeFrom: t.Optional[str] = Field(
+        None,
+        description=(
+            "A starting timestamp of job posting date. Example: 2017-01-01T13:24:56"
+        ),
+        field_type=FieldType.QueryParam,
+    )
+
+    htmlDescription: t.Optional[bool] = Field(
+        None,
+        description=(
+            "A flag to feed the jobs over with html formatting or plain text"
+            " description"
+        ),
+        field_type=FieldType.QueryParam,
+    )
+
+
+def read(
+    adapter: LoggerAdapter,
+    parameters: ReadJobsParameters,
+    read_mode: t.Optional[ReadMode] = None,
+    read_from: t.Optional[str] = None,
+) -> t.Iterable[t.Dict]:
+
+    params = dict()
+    params["companyName"] = parameters.companyName
+    params["parentCompanyName"] = parameters.parentCompanyName
+    params["lastUpdateTimeFrom"] = parameters.lastUpdateTimeFrom
+    params["htmlDescription"] = parameters.htmlDescription
+
+    url = "https://{}.dayforcehcm.com/Api/{}/V1/JobFeeds".format(
+        parameters.subdomain, parameters.client_name_space
+    )
+
+    response = requests.request(
+        "GET",
+        url=url,
+        headers={},
+        data={},
+        params=params,
+    )
+    if response.status_code // 100 != 2:
+        adapter.error(
+            "Failed to pull jobs from Ceridian params={}"
+            " status_code={} response={}".format(
+                params, response.status_code, response.text
+            )
+        )
+        raise Exception("Failed to pull jobs from Ceridian")
+    response = response.json()
+    return response
+
+
+CeridianJobWarehouse = Warehouse(
+    name="Ceridian Jobs",
+    data_schema=CeridianDayforceJobModel,
+    data_type=DataType.job,
+    read=WarehouseReadAction(
+        parameters=ReadJobsParameters,
+        function=read,
+        endpoints=[],
+    ),
+)


Test patch:
diff --git a/src/hrflow_connectors/connectors/ceridian/test-config.yaml b/src/hrflow_connectors/connectors/ceridian/test-config.yaml
new file mode 100644
index 000000000..1a10cdd07
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/test-config.yaml
@@ -0,0 +1,60 @@
+warehouse:
+  CeridianJobWarehouse:
+    read:
+      - parameters:
+          subdomain: $__SUBDOMAIN
+          client_name_space: $__CLIENT_NAME_SPACE
+          posting_status: PUBLIC
+actions:
+  pull_jobs:
+    - id: valid_parameters
+      origin_parameters:
+        subdomain: $__SUBDOMAIN
+        client_name_space: $__CLIENT_NAME_SPACE
+        posting_status: PUBLIC
+      target_parameters:
+        api_secret: $__API_SECRET
+        api_user: $__API_USER
+        board_key: $__BOARD_KEY
+      status: success
+    - id: not_x_smart_token
+      origin_parameters:
+        posting_status: PUBLIC
+      target_parameters:
+        api_secret: $__API_SECRET
+        api_user: $__API_USER
+        board_key: $__BOARD_KEY
+      status: fatal
+      reason: bad_origin_parameters
+    - id: no_hrflow_board_key
+      origin_parameters:
+        subdomain: $__SUBDOMAIN
+        client_name_space: $__CLIENT_NAME_SPACE
+        posting_status: PUBLIC
+      target_parameters:
+        api_secret: $__API_SECRET
+        api_user: $__API_USER
+      status: fatal
+      reason: bad_target_parameters
+    - id: invalid_x_smart_token
+      origin_parameters:
+        subdomain: bad_smart_token
+        client_name_space: bad_namespace
+        posting_status: PUBLIC
+      target_parameters:
+        api_secret: $__API_SECRET
+        api_user: $__API_USER
+        board_key: $__BOARD_KEY
+      status: fatal
+      reason: read_failure
+    - id: invalid_hrflow_api_secret
+      origin_parameters:
+        subdomain: $__SUBDOMAIN
+        client_name_space: $__CLIENT_NAME_SPACE
+        posting_status: PUBLIC
+      target_parameters:
+        api_secret: bad_api_secret
+        api_user: $__API_USER
+        board_key: $__BOARD_KEY
+      status: fatal
+      reason: write_failure