Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 Source Freshdesk: Migrated to latest CDK #12334

Merged
merged 22 commits into from
May 30, 2022

Conversation

lgomezm
Copy link
Contributor

@lgomezm lgomezm commented Apr 26, 2022

What

  • The Freshdesk source connector was written way before the current CDK version and is using outdated patterns.
  • It should be re-written using the latest CDK patterns,

Closes #11456.

How

  • Migrated the source connector to latest CDK.
  • Code is significantly simplified after these changes.
  • Added a few tests to cover new code.

Recommended reading order

  1. source.py
  2. streams.py
  3. Files in unit_tests.

🚨 User Impact 🚨

Are there any breaking changes? What is the end result perceived by the user? If yes, please merge this PR with the 🚨🚨 emoji so changelog authors can further highlight this if needed.

Pre-merge Checklist

Expand the relevant checklist and delete the others.

New Connector

Community member or Airbyter

  • Community member? Grant edit access to maintainers (instructions)
  • Secrets in the connector's spec are annotated with airbyte_secret
  • Unit & integration tests added and passing. Community members, please provide proof of success locally e.g: screenshot or copy-paste unit, integration, and acceptance test output. To run acceptance tests for a Python connector, follow instructions in the README. For java connectors run ./gradlew :airbyte-integrations:connectors:<name>:integrationTest.
  • Code reviews completed
  • Documentation updated
    • Connector's README.md
    • Connector's bootstrap.md. See description and examples
    • docs/SUMMARY.md
    • docs/integrations/<source or destination>/<name>.md including changelog. See changelog example
    • docs/integrations/README.md
    • airbyte-integrations/builds.md
  • PR name follows PR naming conventions

Airbyter

If this is a community PR, the Airbyte engineer reviewing this PR is responsible for the below items.

  • Create a non-forked branch based on this PR and test the below items on it
  • Build is successful
  • If new credentials are required for use in CI, add them to GSM. Instructions.
  • /test connector=connectors/<name> command is passing
  • New Connector version released on Dockerhub by running the /publish command described here
  • After the connector is published, connector added to connector index as described here
  • Seed specs have been re-generated by building the platform and committing the changes to the seed spec files, as described here
Updating a connector

Community member or Airbyter

  • Grant edit access to maintainers (instructions)
  • Secrets in the connector's spec are annotated with airbyte_secret
  • Unit & integration tests added and passing. Community members, please provide proof of success locally e.g: screenshot or copy-paste unit, integration, and acceptance test output. To run acceptance tests for a Python connector, follow instructions in the README. For java connectors run ./gradlew :airbyte-integrations:connectors:<name>:integrationTest.
  • Code reviews completed
  • Documentation updated
    • Connector's README.md
    • Connector's bootstrap.md. See description and examples
    • Changelog updated in docs/integrations/<source or destination>/<name>.md including changelog. See changelog example
  • PR name follows PR naming conventions

Airbyter

If this is a community PR, the Airbyte engineer reviewing this PR is responsible for the below items.

  • Create a non-forked branch based on this PR and test the below items on it
  • Build is successful
  • If new credentials are required for use in CI, add them to GSM. Instructions.
  • /test connector=connectors/<name> command is passing
  • New Connector version released on Dockerhub and connector version bumped by running the /publish command described here
Connector Generator
  • Issue acceptance criteria met
  • PR name follows PR naming conventions
  • If adding a new generator, add it to the list of scaffold modules being tested
  • The generator test modules (all connectors with -scaffold in their name) have been updated with the latest scaffold by running ./gradlew :airbyte-integrations:connector-templates:generator:testScaffoldTemplates then checking in your changes
  • Documentation which references the generator is updated as needed

Tests

Unit

Put your unit tests output here.

Integration

Put your integration tests output here.

Acceptance

Put your acceptance tests output here.

@lgomezm
Copy link
Contributor Author

lgomezm commented Apr 26, 2022

Here's the acceptance test results:
Screen Shot 2022-04-24 at 11 47 59 PM

@lgomezm
Copy link
Contributor Author

lgomezm commented Apr 26, 2022

Here's the unit test results:
Screen Shot 2022-04-25 at 10 39 13 PM

@marcosmarxm
Copy link
Member

marcosmarxm commented Apr 26, 2022

/test connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2228133298
❌ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2228133298
🐛 https://gradle.com/s/q5lb43jc4ja3a

@itaseskii
Copy link
Contributor

I'll start code reviewing this.

@alafanechere
Copy link
Contributor

Thanks @itaseskii , feel free to ping me if you need an additional review.

def streams(self, config: Mapping[str, Any]) -> List[Stream]:
authenticator = self._create_authenticator(config["api_key"])
return [
Agents(authenticator=authenticator, config=config),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't you missing the Surveys stream from the original implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I should have missed it when migrating individual streams. Added back in 4e2f437.

alive = True
error_msg = None
try:
url = f"https://{config['domain'].rstrip('/')}/api/v2/settings/helpdesk"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using urljoin for url concatenation is a bit more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I didn't know about that function. Changed in 4e2f437.

requests_mock.register_uri("GET", "/api/v2/settings/helpdesk", responses)
ok, error_msg = SourceFreshdesk().check_connection(logger, config=config)

assert not ok
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use single line statements/assertions assert not ok and error_msg == ""

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 4e2f437.

self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> Mapping[str, Any]:
return {
"Content-Type": "application/json",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is "Content-Type": "application/json" needed when sending a request? Aren't all the requests of GET type and without a body?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Turns out it works without having to send these headers. I simplified the code by removing this method in 4e2f437.

return "time_entries"


class Tickets(IncrementalFreshdeskStream):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that Tickets will be retrieved in ascending order maybe we can set a state_checkpoint_interval = 100 in order to persist state on every 100 records and avoid sending previously sent records if the replication crashes midway?

https://github.com/airbytehq/airbyte/blob/master/docs/connector-development/cdk-python/incremental-stream.md#interval-based-checkpointing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. I added this in 4e2f437. Thank you for the suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you set state_checkpoint_interval on all the Incremental streams, or you can implement stream_slices. If you implement stream slicing the state will be checkpointed once a slice was properly consumed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in abcae41.

pagination_complete = False

next_page_token = None
with AirbyteSentry.start_transaction("read_records", self.name), AirbyteSentry.start_transaction_span("read_records"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind explaining why copying large amounts of the parent streamread_records()is needed instead of invoking super().read_records()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You made me re-think how I migrated this particular stream and I ended up overriding the next_page_token method instead of the read_records one. In the end, I had to intervene the next_page_token if the next page to get was above 300. The method now has a comment that was ported from the original implementation. See b0bcf0e.

@itaseskii
Copy link
Contributor

Will do another round of code reviewing tomorrow.

yield element


class IncrementalFreshdeskStream(FreshdeskStream, ABC):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class IncrementalFreshdeskStream(FreshdeskStream, ABC):
class IncrementalFreshdeskStream(FreshdeskStream, IncrementalMixin):

The latest CDK introduces the IncrementalMixin class. I suggest you use it and set the state value in read_records as suggested here. get_updated_state is now deprecated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in abcae41

return "time_entries"


class Tickets(IncrementalFreshdeskStream):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you set state_checkpoint_interval on all the Incremental streams, or you can implement stream_slices. If you implement stream slicing the state will be checkpointed once a slice was properly consumed.

alive = True
error_msg = None
try:
url = f"https://{config['domain'].rstrip('/')}/api/v2/settings/helpdesk"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a more robust way of checking connection would be to request several actual streams endpoints

try:
url = f"https://{config['domain'].rstrip('/')}/api/v2/settings/helpdesk"
r = requests.get(url=url, auth=self._create_authenticator(config["api_key"]))
if not r.ok:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest leveraging r.raise_for_status() and handling the error if it's raised.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Updated in 24d1581.

@lgomezm
Copy link
Contributor Author

lgomezm commented May 5, 2022

Hi @alafanechere. Thank you for reviewing. I've updated this PR as per your comments. Please take a look again when you get a chance.

@CLAassistant
Copy link

CLAassistant commented May 5, 2022

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

@alafanechere alafanechere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for another partial review but need to switch context. I'll review the rest asap.

Comment on lines 43 to 44
except ValueError:
error_msg = "Invalid credentials"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which situation ValueError is raised here?
If whatever non HTTPError is raised is I don't think the cause would be Invalid Credential.
I think this try except block is redundant as non HttpError are watched by the higher level except Exception as error:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I've removed the handling of this kind of error.

return alive, error_msg

def streams(self, config: Mapping[str, Any]) -> List[Stream]:
authenticator = self._create_authenticator(config["api_key"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
authenticator = self._create_authenticator(config["api_key"])
authenticator = HTTPBasicAuthNoPassword(config["api_key"])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed authenticator class with FreshdeskAuth and used here.


class HTTPBasicAuthNoPassword(HTTPBasicAuth):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
class HTTPBasicAuthNoPassword(HTTPBasicAuth):
class FreshdeskAuth(HTTPBasicAuth):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

# Since this logic rely not on updated tickets, it can break tickets dependant streams - conversations.
# So updated_since parameter will be always used in tickets streams. And start_date will be used too
# with default value 30 days look back.
self.start_date = pendulum.parse(start_date) if start_date else pendulum.now() - pendulum.duration(days=30)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.start_date = pendulum.parse(start_date) if start_date else pendulum.now() - pendulum.duration(days=30)
self.start_date = pendulum.parse(config["start_date"]) if config["start_date"] else pendulum.now() - pendulum.duration(days=30)


class FreshdeskStream(HttpStream, ABC):
"""Basic stream API that allows to iterate over entities"""
call_credit = 1 # see https://developers.freshdesk.com/api/#embedding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value never changes, I'd prefer to decrement from 1 rather than decrementing from this variable if it never changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked and it was actually being used with a different value in the Tickets stream. I've updated it accordingly.


def backoff_time(self, response: requests.Response) -> Optional[float]:
if response.status_code == requests.codes.too_many_requests:
return float(response.headers.get("Retry-After"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return float(response.headers.get("Retry-After"))
return float(response.headers.get("Retry-After", 0))

You will have a TypeError if Retry-After is not in the headers: float(None) -> TypeError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Done.

params = parse.parse_qs(parse.urlparse(next_url).query)
return self.parse_link_params(link_query_params=params)
except Exception as e:
raise KeyError(f"error parsing next_page token: {e}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose to raise a KeyError here?
Could you please explain what kind of error you expect in this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I updated this method and removed the exception handling.

match = self.link_regex.search(link_header)
next_url = match.group(1)
params = parse.parse_qs(parse.urlparse(next_url).query)
return self.parse_link_params(link_query_params=params)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return self.parse_link_params(link_query_params=params)
return {"per_page": params['per_page'][0], "page": params['page'][0]}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 59 to 60
def parse_link_params(self, link_query_params: Mapping[str, List[str]]) -> Mapping[str, Any]:
return {"per_page": link_query_params['per_page'][0], "page": link_query_params['page'][0]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def parse_link_params(self, link_query_params: Mapping[str, List[str]]) -> Mapping[str, Any]:
return {"per_page": link_query_params['per_page'][0], "page": link_query_params['page'][0]}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None
) -> MutableMapping[str, Any]:
params = {"per_page": self.result_return_limit}
if next_page_token and "page" in next_page_token:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could page not be in next_page_token if next_page_token is not None?

Copy link
Contributor

@alafanechere alafanechere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm done reviewing source.py and streams.py :) I made a lot of suggestions to simplify and increase readability + match CDK best practices.

@lgomezm lgomezm force-pushed the lgomez/freshdesk_migration branch from 24d1581 to 5751b4a Compare May 18, 2022 02:31
@lgomezm
Copy link
Contributor Author

lgomezm commented May 18, 2022

@alafanechere I've addressed your comments. Please take a look again when you get a chance and let me know if there's anything else you consider I should update. Thanks!

@alafanechere
Copy link
Contributor

alafanechere commented May 18, 2022

/test connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2346034912
❌ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2346034912
🐛 https://gradle.com/s/uwca5iuwfwemi

@alafanechere
Copy link
Contributor

Thank you for the changes @lgomezm , I'm running the test now and will go for a final review asap.

@alafanechere
Copy link
Contributor

alafanechere commented May 18, 2022

/test connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2346404839
❌ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2346404839
🐛 https://gradle.com/s/x6peupa7oyhoi
Python short test summary info:

=========================== short test summary info ============================
FAILED test_core.py::TestDiscovery::test_discover[inputs0] - docker.errors.Co...
ERROR test_core.py::TestDiscovery::test_defined_cursors_exist_in_schema[inputs0]
ERROR test_core.py::TestDiscovery::test_defined_refs_exist_in_schema[inputs0]
ERROR test_core.py::TestDiscovery::test_defined_keyword_exist_in_schema[inputs0-allOf]
ERROR test_core.py::TestDiscovery::test_defined_keyword_exist_in_schema[inputs0-not]
ERROR test_core.py::TestDiscovery::test_primary_keys_exist_in_schema[inputs0]
ERROR test_core.py::TestBasicRead::test_read[inputs0] - docker.errors.Contain...
ERROR test_full_refresh.py::TestFullRefresh::test_sequential_reads[inputs0]
ERROR test_incremental.py::TestIncremental::test_two_sequential_reads[inputs0]
ERROR test_incremental.py::TestIncremental::test_state_with_abnormally_large_values[inputs0]
=================== 1 failed, 13 passed, 9 errors in 21.75s ====================

Copy link
Contributor

@alafanechere alafanechere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lgomezm the acceptance tests are not passing. Could you please provide the required fixes? Feel free to ask if you need help understanding why the tests are not passing.

@lgomezm
Copy link
Contributor Author

lgomezm commented May 20, 2022

Hi @alafanechere. I've pushed a change that fixes acceptance tests. Please give it a try again when you get a chance. Here's the results of them running locally:
Screen Shot 2022-05-19 at 10 39 25 PM

@alafanechere
Copy link
Contributor

alafanechere commented May 20, 2022

/test connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2360495709
❌ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2360495709
🐛 https://gradle.com/s/kd4252r2tpsm2
Python short test summary info:

=========================== short test summary info ============================
FAILED test_core.py::TestBasicRead::test_read[inputs0] - docker.errors.Contai...
FAILED test_full_refresh.py::TestFullRefresh::test_sequential_reads[inputs0]
FAILED test_incremental.py::TestIncremental::test_two_sequential_reads[inputs0]
FAILED test_incremental.py::TestIncremental::test_read_sequential_slices[inputs0]
=================== 4 failed, 20 passed in 177.99s (0:02:57) ===================

@alafanechere
Copy link
Contributor

We are facing an error while running the acceptance test with our Freshdesk account:

The Skill Based Round Robin feature(s) is/are not supported in your plan. Please upgrade your account to use it.", "internal_message": "403 Client Error: Forbidden for url: https://newaccount1603334233301.freshdesk.com/api/skills?per_page=100

I'm trying to remove the Skills stream from the tests.

@alafanechere
Copy link
Contributor

alafanechere commented May 23, 2022

/test connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2369927218
❌ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2369927218
🐛 https://gradle.com/s/lfnrq3374ggh2
Python short test summary info:

=========================== short test summary info ============================
FAILED test_core.py::TestBasicRead::test_read[inputs0] - Failed: Timeout >300.0s
FAILED test_full_refresh.py::TestFullRefresh::test_sequential_reads[inputs0]
FAILED test_incremental.py::TestIncremental::test_two_sequential_reads[inputs0]
FAILED test_incremental.py::TestIncremental::test_read_sequential_slices[inputs0]
=================== 4 failed, 20 passed in 989.46s (0:16:29) ===================

@alafanechere
Copy link
Contributor

@lgomezm I'll be off until next Monday. Could you please try to figure out what could be problematic with the Skills stream and try to ignore it from the integration tests if needed (it looks like my latest attempt in 77ea41b did not work)?

@lgomezm lgomezm force-pushed the lgomez/freshdesk_migration branch from 77ea41b to 9cf2ba1 Compare May 27, 2022 02:59
@lgomezm
Copy link
Contributor Author

lgomezm commented May 27, 2022

Hi @alafanechere. It looks like the test account that's used to run integration tests is not allowed to get skills. I have overwritten the configured catalog file so that the skills stream is not included in the acceptance tests. They should pass now.

@alafanechere
Copy link
Contributor

alafanechere commented May 30, 2022

/test connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2407311353
✅ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2407311353
Python tests coverage:

Name                                                 Stmts   Miss  Cover
------------------------------------------------------------------------
source_acceptance_test/utils/__init__.py                 6      0   100%
source_acceptance_test/tests/__init__.py                 4      0   100%
source_acceptance_test/__init__.py                       2      0   100%
source_acceptance_test/tests/test_full_refresh.py       52      2    96%
source_acceptance_test/utils/asserts.py                 37      2    95%
source_acceptance_test/config.py                        77      6    92%
source_acceptance_test/utils/json_schema_helper.py     105     13    88%
source_acceptance_test/tests/test_incremental.py       121     25    79%
source_acceptance_test/utils/common.py                  80     17    79%
source_acceptance_test/tests/test_core.py              294    106    64%
source_acceptance_test/utils/compare.py                 62     23    63%
source_acceptance_test/base.py                          10      4    60%
source_acceptance_test/utils/connector_runner.py       110     48    56%
------------------------------------------------------------------------
TOTAL                                                  960    246    74%
Name                           Stmts   Miss  Cover
--------------------------------------------------
source_freshdesk/source.py        31      0   100%
source_freshdesk/__init__.py       2      0   100%
source_freshdesk/streams.py      139      4    97%
source_freshdesk/utils.py         20      1    95%
--------------------------------------------------
TOTAL                            192      5    97%

Build Passed

Test summary info:

All Passed

@alafanechere
Copy link
Contributor

Hey @lgomezm while I was off our team upgraded our freshdesk account to overcome the problem on the skills account (#13158)

@github-actions github-actions bot added the area/documentation Improvements or additions to documentation label May 30, 2022
@alafanechere
Copy link
Contributor

alafanechere commented May 30, 2022

/publish connector=connectors/source-freshdesk

🕑 connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2408021884
🚀 Successfully published connectors/source-freshdesk
🚀 Auto-bumped version for connectors/source-freshdesk
✅ connectors/source-freshdesk https://github.com/airbytehq/airbyte/actions/runs/2408021884

Copy link
Contributor

@alafanechere alafanechere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution @lgomezm !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/connectors Connector related issues area/documentation Improvements or additions to documentation community
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Source Freshdesk: migrate to use CDK
6 participants