Getting this error while trying to run `meltano in...
# singer-tap-development
i
Getting this error while trying to run
meltano invoke tap-mytap
ValueError: Missing 'private_key' property for OAuth payload. My property looks like this
Copy code
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 properties
r
Can you show your
authenticator
implementation?
āž• 1
Also, I assume you have
private_key
set in config somewhere?
i
Copy code
class 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.
My API "/api/authentication" endpoint accepts client_id, username, and password which I've included in the properties and set in the meltano.yml. The auth endpoint accepts those and then generates a token to be included in the header: "Authorization" of each request.
So nowhere in there is there a private_key that's required. I wanted to just replace the logic and use my own logic to generate the token - but then I was getting conflicting answers on where to put that logic - either as a new class or within the JobDivaAuthenticator class
r
OAuthJWTAuthenticator
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-L569
šŸ‘ 2
It sounds like you might want to subclass OAuthAuthenticator and add support for
username
and
password
through
oauth_request_body
implementation: https://github.com/meltano/sdk/blob/b7b8d156a4ea6dec6907fa0fb8bed0347eae0c6b/singer_sdk/authenticators.py#L428-L452
šŸ‘ 1
i
So bascially just inheret from the OAuthAuthenticator class instead of the OAuthJWTAuthenticator class, keeping else the same - since OAA doesn't require the private key property?
r
Yes, but you need to provide
ouath_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).
i
Copy code
class 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?
r
I think so, yes.
i
ERROR    | 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?
r
That error is coming from a config property you defined in
tap.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.
i
you mean remove it as a config property in my tap.py or in my meltano.yml?
r
Both!
šŸ™Œ 1
i
Could not append type because the JSON schema for the dictionary
{'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?
r
Weird... I've seen those schema warnings before in one of my own taps:
Copy code
WARNING: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?
i
Copy code
tap-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>
Copy code
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.
r
405
is
Method Not Allowed
- it making the correct type of request (i.e.
POST
or
GET
)?
i
It's supposed to use GET, but I haven't defined that method anywhere
r
OAuthAuthenticator
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
.
i
so - just re-declare the
update_access_token
within the JobDiva auth class similarly to the
oauth_request_body
as before, but with requests.get instead of post?
r
Yep, exactly.
i
should I declare it as a @classmethod? or does it matter
Copy code
tap-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 fixed
r
The base implementation is not a
@classmethod
, so leave it off.
i
got it
Copy code
RuntimeError: 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.
Copy code
@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
Copy code
- 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?
r
Run
Copy code
meltano 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:
Copy code
TAP_JOBDIVA_USERNAME=<username>
TAP_JOBDIVA_PASSWORD=<password>
TAP_JOBDIVA_CLIENT_ID=<client id>
šŸ‘ 1
Also just to check, is your API expecting
clientid
in the request body or as a URL parameter?
i
as a parameter
r
Are
username
,
password
and
client_id
all URL parameters?
i
yes, for the auth endpoint
r
OK, then you will need to do something like:
Copy code
@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
i
the auth attemp was successful - woohoo! but getting this json decoder failure now
Copy code
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)
is that from the api response ?
r
What does the response body from the API look like? If it's not JSON, you will need to modify
Copy code
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.",
            )
i
Copy code
{
  "message": "Date range more than 14 days, metric name = NewUpdatedCandidateRecords",
  "data": {}
}
data includes the dataset in json
r
The authorization response?
i
oh no thats the stream response, the authorization response is just a string of text which is the token
r
Yeah, so you need to do
response.text
instead of
response.json()
.
i
do you know if the expiration is usually in seconds? the tokens last 24 hours
r
Yeah, so it would be 86400 seconds.
i
Copy code
streams\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?
r
Can you share that stream class?
i
Copy code
class NewUpdatedCandidateRecordsStream(JobDivaStream):
    """Define custom stream."""

    name = "NewUpdatedCandidateRecordsStream"
    path = "bi/NewUpdatedCandidateRecords"
here's the url base
Copy code
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/>"
Copy code
streams\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 error
r
Are you assigning
self.access_token
in
update_access_token
?
i
Copy code
token_json = token_response.text
self.access_token = token_json
expiration = 86400
i know this is redundant, but yes
r
OK, so the stream request to
/apiv2/bi/NewUpdatedCandidateRecords
should be using the token, assuming you have set the authenticator for the client. It's just a
GET
request right?
i
Copy code
@cached_property
    def authenticator(self) -> _Auth:
        """Return a new authenticator object.

        Returns:
            An authenticator instance.
        """
        return JobDivaAuthenticator.create_for_stream(self)
yup
Copy code
@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?
my oath_scopes is set to the /bi endpoint
Im not sure the API requires scopes so I just did that
r
It looks fine, but I don't know the API you're working with here so it's hard to say. If you are getting a token back, then it is probably fine... Can you make the request manually from Postman or
curl
if you have a token?
i
curl worked. the stream requires fromDate and toDate params, though - that's probably why its failing
r
i
so is "limit" your only parameter there?
r
limit
, and
offset
when paginating.
i
gotcha. so the params/logic I define in the client.py stream class should apply to all streams that inheret that class in streams.py?
r
Yep, and you can override at the stream level too (e.g. if one if your endpoints requires different/extra params).
i
@Reuben (Matatika) Speaking of pagination, do you have any experience paginating with dates?
r
@Ian OLeary FYI I think the warnings
Copy code
WARNING: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
šŸ™Œ 1