Ian OLeary
02/08/2024, 10:44 PMmeltano invoke tap-mytap
ValueError: Missing 'private_key' property for OAuth payload.
My property looks like this
th.Property(
"private_key",
th.StringType,
default=None,
description="Private_key",
),
I'm using OAuthJWTAuthenticator and have provided client_id, username, and password as other propertiesReuben (Matatika)
02/08/2024, 10:48 PMauthenticator
implementation?Reuben (Matatika)
02/08/2024, 10:56 PMprivate_key
set in config somewhere?Ian OLeary
02/09/2024, 2:49 PMclass JobDivaAuthenticator(OAuthJWTAuthenticator):
"""Authenticator class for JobDiva."""
@classmethod
def create_for_stream(
cls,
stream, # noqa: ANN001
) -> JobDivaAuthenticator:
"""Instantiate an authenticator for a specific Singer stream.
Args:
stream: The Singer stream instance.
Returns:
A new authenticator.
"""
return cls(
stream=stream,
auth_endpoint=JOBDIVA_AUTH_ENDPOINT,
oauth_scopes=JOBDIVA_AUTH_SCOPES,
)
So I don't believe I need a private key to authenticate which is what confuses me. I have it set to default=None and it's just a blank string in my meltano.yml. I also got confused with how to implement the authenticator - and just re-scaffolded the tap from scratch.Ian OLeary
02/09/2024, 2:52 PMIan OLeary
02/09/2024, 2:53 PMReuben (Matatika)
02/09/2024, 2:54 PMOAuthJWTAuthenticator
expects a private_key
, and therefore so does JobDivaAuthenticator
since you are extending from that class: https://github.com/meltano/sdk/blob/b7b8d156a4ea6dec6907fa0fb8bed0347eae0c6b/singer_sdk/authenticators.py#L567-L569Reuben (Matatika)
02/09/2024, 3:00 PMusername
and password
through oauth_request_body
implementation: https://github.com/meltano/sdk/blob/b7b8d156a4ea6dec6907fa0fb8bed0347eae0c6b/singer_sdk/authenticators.py#L428-L452Ian OLeary
02/09/2024, 3:25 PMReuben (Matatika)
02/09/2024, 3:28 PMouath_request_body
(this is what OAuthJWTAuthenticator
is doing, since OAuthAuthenticator
enforces a subclass implementation). In that implementation, you can add logic for handling username
and password
(see this sample).Ian OLeary
02/09/2024, 3:31 PMclass JobDivaAuthenticator(OAuthAuthenticator):
"""Authenticator class for JobDiva."""
@property
def oauth_request_body(self) -> dict:
return {
"grant_type": "password",
"scope": "<https://api.powerbi.com>",
"resource": "<https://analysis.windows.net/powerbi/api>",
"client_id": self.config["client_id"],
"username": self.config.get("username", self.config["client_id"]),
"password": self.config["password"],
}
@classmethod
...
...
So that logic will look something like this, correct?Reuben (Matatika)
02/09/2024, 3:33 PMIan OLeary
02/09/2024, 4:32 PMERROR | tap-jobdiva | Config validation error: 'auth_token' is a required property
So I defined that logic in the authenticator class, but I'm now getting this error. My auth_token is set to "None" as default within properties. Is this auth_token supposed to represent the token generated by the authentication endpoint?Reuben (Matatika)
02/09/2024, 4:38 PMtap.py
that you are not providing (it's probably required=True
).
> Is this auth_token supposed to represent the token generated by the authentication endpoint
Yes, but that should be handled by JobDivaAuthenticator
, given that you've provided a valid auth_endpoint
- all that to say I would remove it as a config property.Ian OLeary
02/09/2024, 4:40 PMReuben (Matatika)
02/09/2024, 4:41 PMIan OLeary
02/09/2024, 4:50 PM{'oneOf': [{'type': ['number']}, {'type': ['string']}, {'type': ['boolean']}]}
appears to be invalid.
Could not append type because the JSON schema for the dictionary {'oneOf': [{'type': ['string']}, {'type': 'array', 'items': {'type': ['string']}}]}
appears to be invalid.
2024-02-09 114105,804 | INFO | tap-jobdiva.NewUpdatedCandidateRecordsStream | Beginning full_table sync of 'NewUpdatedCandidateRecordsStream'...
2024-02-09 114105,805 | INFO | tap-jobdiva.NewUpdatedCandidateRecordsStream | Tap has custom mapper. Using 1 provided map(s).
{"type":"SCHEMA","stream":"NewUpdatedCandidateRecordsStream","schema":{"properties":{"CANDIDATEID":{"description":"This is the primary key for the table","type":["integer","null"]},"FIRSTNAME":{"type":["string","null"]},"LASTNAME":{"type":["string","null"]},"DATECREATED":{"format":"date-time","type":["string","null"]},"DATEUPDATED":{"format":"date-time","type":["string","null"]},"EMAIL":{"type":["string","null"]}},"type":"object"},"key_properties":["CANDIDATEID"]}
So I'm now getting this error - you think this is being caused by my stream class schema? or by the schema being returned from the api?Reuben (Matatika)
02/09/2024, 5:22 PMWARNING:Could not append type because the JSON schema for the dictionary `{'oneOf': [{'type': ['number']}, {'type': ['string']}, {'type': ['boolean']}]}` appears to be invalid.
WARNING:Could not append type because the JSON schema for the dictionary `{'oneOf': [{'type': ['string']}, {'type': 'array', 'items': {'type': ['string']}}]}` appears to be invalid.
They are just warnings though - are you actually encountering an error?Ian OLeary
02/09/2024, 5:26 PMtap-jobdiva\venv\lib\site-packages\requests\models.py", line 1021, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 405 Client Error: for url: <https://api.jobdiva.com/apiv2/authenticate>
tap-jobdiva\venv\lib\site-packages\simplejson\decoder.py", line 416, in raw_decode
return self.scan_once(s, idx=_w(s, idx).end())
simplejson.errors.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Getting both of these later on. The 405 Client Error isn't in the list of response codes the API can generate, so I wonder what's causing this.Reuben (Matatika)
02/09/2024, 5:27 PM405
is Method Not Allowed
- it making the correct type of request (i.e. POST
or GET
)?Ian OLeary
02/09/2024, 5:29 PMReuben (Matatika)
02/09/2024, 5:30 PMOAuthAuthenticator
is using `POST`: https://github.com/meltano/sdk/blob/b7b8d156a4ea6dec6907fa0fb8bed0347eae0c6b/singer_sdk/authenticators.py#L493-L498.
You're probably going to have to override update_access_token
for it to use GET
.Ian OLeary
02/09/2024, 5:34 PMupdate_access_token
within the JobDiva auth class similarly to the oauth_request_body
as before, but with requests.get instead of post?Reuben (Matatika)
02/09/2024, 5:36 PMIan OLeary
02/09/2024, 5:38 PMIan OLeary
02/09/2024, 5:49 PMtap-jobdiva\tap_jobdiva\auth.py", line 36, in update_access_token
headers=self._oauth_headers,
AttributeError: type object 'JobDivaAuthenticator' has no attribute '_oauth_headers'. Did you mean: 'auth_headers'?
now getting this error - the other one was fixedReuben (Matatika)
02/09/2024, 5:50 PM@classmethod
, so leave it off.Ian OLeary
02/09/2024, 6:06 PMIan OLeary
02/09/2024, 6:09 PMRuntimeError: Failed OAuth login, response was '{'apierror': {'status': 'BAD_REQUEST', 'timestamp': '09-02-2024 12:53:13', 'message': 'clientid parameter is missing', 'debugMessage': "Required Long parameter 'clientid' is not present", 'subErrors': None}}'. 400 Client Error: for url: <https://api.jobdiva.com/apiv2/authenticate>
so now at least it looks like it's getting to the endpoint - but not accepting my "client_id" parameter.
@property
def oauth_request_body(self) -> dict:
return {
"grant_type": "password",
"scope": JOBDIVA_AUTH_SCOPES,
"resource": JOBDIVA_AUTH_ENDPOINT,
"clientid": self.config["client_id"],
"username": self.config["username"],
"password": self.config["password"],
}
here's what my oauth_request_body looks like. all these are set as properties, with my meltano.yml setting them from env variables
- name: username
value: $JOBDIVA_USERNAME
- name: password
kind: password
value: $JOBDIVA_PASSWORD
- name: client_id
value: $JOBDIVA_CLIENTID
am I setting the env variables wrong?Reuben (Matatika)
02/09/2024, 6:27 PMmeltano config tap-jobdiva list
to see what config values are being resolved.
I would avoid defining default values for your settings like that though, since Meltano already exposes your settings for configuration via environment variables in the format `TAP_NAME_SETTING_NAME`:
⢠TAP_JOBDIVA_USERNAME
⢠TAP_JOBDIVA_PASSWORD
⢠TAP_JOBDIVA_CLIENT_ID
Then you can just create a .env
file and modify that as you go:
TAP_JOBDIVA_USERNAME=<username>
TAP_JOBDIVA_PASSWORD=<password>
TAP_JOBDIVA_CLIENT_ID=<client id>
Reuben (Matatika)
02/09/2024, 6:32 PMclientid
in the request body or as a URL parameter?Ian OLeary
02/09/2024, 7:06 PMReuben (Matatika)
02/09/2024, 7:25 PMusername
, password
and client_id
all URL parameters?Ian OLeary
02/09/2024, 7:25 PMReuben (Matatika)
02/09/2024, 7:32 PM@property
def oauth_request_body(self) -> dict:
return {
"grant_type": "password",
"scope": JOBDIVA_AUTH_SCOPES,
"resource": JOBDIVA_AUTH_ENDPOINT,
}
def update_access_token(self) -> None:
"""Update `access_token` along with: `last_refreshed` and `expires_in`.
Raises:
RuntimeError: When OAuth login fails.
"""
request_time = utc_now()
auth_request_payload = self.oauth_request_payload
token_response = requests.get(
self.auth_endpoint,
headers=self._oauth_headers,
params={
"clientid": config["client_id"],
"username": config["username"],
"password": config["password"],
},
data=auth_request_payload,
timeout=60,
)
try:
token_response.raise_for_status()
except requests.HTTPError as ex:
msg = f"Failed OAuth login, response was '{token_response.json()}'. {ex}"
raise RuntimeError(msg) from ex
<http://self.logger.info|self.logger.info>("OAuth authorization attempt was successful.")
token_json = token_response.json()
self.access_token = token_json["access_token"]
expiration = token_json.get("expires_in", self._default_expiration)
self.expires_in = int(expiration) if expiration else None
if self.expires_in is None:
self.logger.debug(
"No expires_in received in OAuth response and no "
"default_expiration set. Token will be treated as if it never "
"expires.",
)
self.last_refreshed = request_time
Ian OLeary
02/09/2024, 7:45 PMtap-jobdiva\venv\lib\site-packages\simplejson\decoder.py", line 416, in raw_decode
return self.scan_once(s, idx=_w(s, idx).end())
simplejson.errors.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
is that from the api response ?Reuben (Matatika)
02/09/2024, 7:47 PMtoken_json = token_response.json()
self.access_token = token_json["access_token"]
expiration = token_json.get("expires_in", self._default_expiration)
self.expires_in = int(expiration) if expiration else None
if self.expires_in is None:
self.logger.debug(
"No expires_in received in OAuth response and no "
"default_expiration set. Token will be treated as if it never "
"expires.",
)
Ian OLeary
02/09/2024, 7:48 PM{
"message": "Date range more than 14 days, metric name = NewUpdatedCandidateRecords",
"data": {}
}
Ian OLeary
02/09/2024, 7:48 PMReuben (Matatika)
02/09/2024, 7:49 PMIan OLeary
02/09/2024, 7:49 PMReuben (Matatika)
02/09/2024, 7:50 PMresponse.text
instead of response.json()
.Ian OLeary
02/09/2024, 7:58 PMReuben (Matatika)
02/09/2024, 8:01 PMIan OLeary
02/09/2024, 8:09 PMstreams\rest.py", line 187, in validate_response
raise FatalAPIError(msg)
singer_sdk.exceptions.FatalAPIError: 404 Client Error: for path: /apiv2/<https://api.jobdiva.com/apiv2/bi/NewUpdatedCandidateRecordsStream>
I'm now getting this - Is it prepending the /apiv2/ to the path?Reuben (Matatika)
02/09/2024, 8:13 PMIan OLeary
02/09/2024, 8:15 PMclass NewUpdatedCandidateRecordsStream(JobDivaStream):
"""Define custom stream."""
name = "NewUpdatedCandidateRecordsStream"
path = "bi/NewUpdatedCandidateRecords"
here's the url base
class JobDivaStream(RESTStream):
"""JobDiva stream class."""
@property
def url_base(self) -> str:
"""Return the API URL root, configurable via tap settings."""
# TODO: hardcode a value here, or retrieve it from self.config
return "<https://api.jobdiva.com/apiv2/>"
Ian OLeary
02/09/2024, 8:18 PMstreams\rest.py", line 187, in validate_response
raise FatalAPIError(msg)
singer_sdk.exceptions.FatalAPIError: 400 Client Error: for path: /apiv2/bi/NewUpdatedCandidateRecords
okay so I fixed the stream path (whoops) but now getting this errorReuben (Matatika)
02/09/2024, 8:22 PMself.access_token
in update_access_token
?Ian OLeary
02/09/2024, 8:24 PMtoken_json = token_response.text
self.access_token = token_json
expiration = 86400
Ian OLeary
02/09/2024, 8:24 PMReuben (Matatika)
02/09/2024, 8:25 PM/apiv2/bi/NewUpdatedCandidateRecords
should be using the token, assuming you have set the authenticator for the client. It's just a GET
request right?Ian OLeary
02/09/2024, 8:28 PM@cached_property
def authenticator(self) -> _Auth:
"""Return a new authenticator object.
Returns:
An authenticator instance.
"""
return JobDivaAuthenticator.create_for_stream(self)
Ian OLeary
02/09/2024, 8:28 PMIan OLeary
02/09/2024, 8:29 PM@classmethod
def create_for_stream(
cls,
stream, # noqa: ANN001
) -> JobDivaAuthenticator:
"""Instantiate an authenticator for a specific Singer stream.
Args:
stream: The Singer stream instance.
Returns:
A new authenticator.
"""
return cls(
stream=stream,
auth_endpoint=JOBDIVA_AUTH_ENDPOINT,
oauth_scopes=JOBDIVA_AUTH_SCOPES,
)
does my create_for_stream look correct?Ian OLeary
02/09/2024, 8:29 PMIan OLeary
02/09/2024, 8:30 PMReuben (Matatika)
02/09/2024, 8:36 PMcurl
if you have a token?Ian OLeary
02/09/2024, 8:44 PMReuben (Matatika)
02/09/2024, 8:48 PMIan OLeary
02/09/2024, 8:58 PMReuben (Matatika)
02/09/2024, 9:01 PMlimit
, and offset
when paginating.Ian OLeary
02/09/2024, 9:05 PMReuben (Matatika)
02/09/2024, 9:13 PMIan OLeary
02/13/2024, 4:49 PMReuben (Matatika)
02/13/2024, 5:01 PMReuben (Matatika)
02/16/2024, 10:07 AMWARNING:Could not append type because the JSON schema for the dictionary `{'oneOf': [{'type': ['number']}, {'type': ['string']}, {'type': ['boolean']}]}` appears to be invalid.
WARNING:Could not append type because the JSON schema for the dictionary `{'oneOf': [{'type': ['string']}, {'type': 'array', 'items': {'type': ['string']}}]}` appears to be invalid.
have been fixed in SDK `v0.35.1`: https://github.com/meltano/sdk/pull/2245