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

Made it work with the 2023-10 version of the API #116

Merged
merged 24 commits into from
Nov 22, 2023

Conversation

willnaoosmith
Copy link
Contributor

Hi there!

I made this wrapper work the new API, which will be live on 17 of January 2024.
There are some other changes that are from the repository that I forked, since it was more recent than the original repository.

This should fix (or at least help with) the #115 issue.
Tested on a test board and it works fine!

Changes I made:

  1. The headers option, that I mentioned before (Will not be required to use the new API version after 17 of January).
monday = MondayClient(
    token = "YourTokenHere",
    headers = {'API-Version' : '2023-10'}
)
  1. The fetch_updates_for_item query doesn't use the board ID parameter anymore, so it should be like this:
monday.updates.fetch_updates_for_item(item_id="ItemIDHere", limit="LimitHere")
  1. the fetch_items_by_column_value uses items_page_by_column_values now instead of the items_by_column_values, but this does not change anything about how it should be used.
monday.items.fetch_items_by_column_value(board_id='BoardIDHere',  column_id="ColumnIDHere", value="ColumnValueHere")

To test it, install it by running pip install git+https://github.com/willnaoosmith/monday.git and change the headers to use the 2023-10 version.

albcl and others added 24 commits June 18, 2022 19:12
Added: Exception for catching Monday API query errors
@chdastolfo
Copy link
Collaborator

@willnaoosmith Thanks so much for doing this!! We'll take a look ASAP!

@pevner-p2 pevner-p2 requested a review from rhymiz November 9, 2023 23:13
Copy link
Collaborator

@pevner-p2 pevner-p2 left a comment

Choose a reason for hiding this comment

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

@chdastolfo, I'm generally 👍 with these changes, though I'm mostly praying that the tests are reliable and trusting that the author has tested sufficiently on the new API version.

However, there's some formatting stuff that's upsetting the ProdPerfect linter. This is coming from the author's private fork, so I can't just mess fix it up on that branch, and I'm not going to ask them to fix up stuff specific to our linting rules. Instead, I assume we'll accept this PR, and then I'll throw up another PR just for the formatting stuff. Maybe that can include the deployment related changes (e.g. updated contributor list, incremented version).

Hopefully we can coordinate a deployment soon, maybe we bring in rhymiz if you want?

Anyway, I've left a few comments, nothing blocking, most of them are just me ranting. Glad to get this cooking.

return response.json()
except requests.HTTPError as e:
print(e)
raise e
except MondayQueryError as ex:
Copy link
Collaborator

Choose a reason for hiding this comment

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

[non-blocking] This could be collapsed into the previous except block using a tuple, i.e. except (requests.HTTPError, MondayQueryError) as e.



class MondayClient:
def __init__(self, token):
def __init__(self, token, headers={}):
Copy link
Collaborator

Choose a reason for hiding this comment

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

[non-blocking] Given that this header is there primarily to handle the API change, are we planning on getting rid of this header parameter after Monday removes the old API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They probably will still update the API, and still let us set the API version on the headers, so at least I think it would be helpful to have the option to set the headers to every request nonetheless.

We depend on them to make the changes, so we'll never know.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, looks like this is the only commented code that I actually made, so this is the only one I can say about.

Copy link

Choose a reason for hiding this comment

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

Defaulting a parameter to a mutable value is very dangerous:

client = MondayClient(token = "abc")
client2 = MondayClient(token = "MMM")

client.boards.fetch_columns_by_board_id("dsadsaa")
client2.boards.client.headers # {'Authorization': 'abc', 'Content-Type': 'application/json'}  wrong API key

Copy link

Choose a reason for hiding this comment

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

An alternative would be to pass the desired version api_version="2023-10" to the constructor

Happy to hear your thoughts but here's why I think it'd be better:

  1. It makes it more obvious what we're trying to achieve with this param, since that's the only (current) logical use for the headers, outside of internal stuff like Content-Type etc.
  2. By keeping the version on e.g. self.api_version, it would allow to keep the old stuff working if someone wants to use the old API for the time being. For example the added items_page() in queries could be added only if self.api_version == '2023-10' etc
  3. See my comment above about mutating a default class param. A string doesn't have that issue
  4. Users of the lib don't need to know what the headers look like (yes, it's very simple to document, but still)

Copy link
Collaborator

@pevner-p2 pevner-p2 Nov 13, 2023

Choose a reason for hiding this comment

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

My $0.02: I'm still of the opinion that we should consider burning this parameter entirely come 16 Jan 2024, and have library version labels indicating what's happening.

Given that Monday is dropping support for the old version on 15 Jan, this would be a legacy parameter 3 months after release. There's certainly a chance that they'll do more versioning in the future, but I'd rather be handling it implicitly, through our client library versioning, than try to support multiple API versions in the same client library. I appreciate that their future versioning might be more incremental, but given that they're using date versioning, rather than semantic versioning, they're permitted hard breaks with each iteration.

Also, this is how I wish we were doing it now, but I'm not one to look a gift PR in the mouth, given that we've got a relatively short timeline for cutting this library over.

Copy link

Choose a reason for hiding this comment

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

My $0.02: I'm still of the opinion that we should consider burning this parameter entirely come 16 Jan 2024, and have library version labels indicating what's happening.

Given that Monday is dropping support for the old version on 15 Jan, this would be a legacy parameter 3 months after release. There's certainly a chance that they'll do more versioning in the future, but I'd rather be handling it implicitly, through our client library versioning, than try to support multiple API versions in the same client library. I appreciate that their future versioning might be more incremental, but given that they're using date versioning, rather than semantic versioning, they're permitted hard breaks with each iteration.

Also, this is how I wish we were doing it now, but I'm not one to look a gift PR in the mouth, given that we've got a relatively short timeline for cutting this library over.

I see your point, because they are literally deprecating the old API. Maybe there is simply no need for an extra parameter. In any case, it should definitely not be declared with a mutable dict as default params value :)

@@ -1,2 +1,6 @@
class MondayError(RuntimeError):
pass


class MondayQueryError(Exception):
Copy link
Collaborator

@pevner-p2 pevner-p2 Nov 9, 2023

Choose a reason for hiding this comment

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

[very non-blocking] I don't know why we inherit from RuntimeError for MondayError, but maybe we should follow that pattern? I think it's off, personally, but I don't have the context. Alternately, this could inherit from MondayError, but that also seems wrong, as it doesn't seem to be used anywhere. Maybe it can go away? I don't know.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I genuinely do not know or remember. That can almost certainly go away.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Technically, MondayError (base exception class for this library) should inherit from Exception and MondayQueryError should inherit from MondayError. This makes it very easy to handle "all" potential exceptions that may be thrown inside the monday client by calling code:

try:
   pass
except MondayError:
   pass

folder_id = folder_id if folder_id else None
keep_subscribers = keep_subscribers if keep_subscribers else False

params = """board_id: %s, duplicate_type: %s, board_name: \"%s\"""" % (
Copy link
Collaborator

Choose a reason for hiding this comment

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

[very non-blocking] Why does no one in this repo use f-strings?!

[edit] And then, randomly, later on, they do!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

The non f-strings were almost certainly my fault when I first wrote this, what feels like eons ago, and then f-strings were added later by either myself or contributors. I've been meaning to go through and clean this up.

):
query = duplicate_board_query(board_id, duplicate_type, board_name, workspace_id, folder_id, keep_subscribers)
return self.client.execute(query)
return self.client.execute(query)
Copy link
Collaborator

Choose a reason for hiding this comment

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

[non-blocking, but c'mon] Dupe.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, can we delete this?

@willnaoosmith
Copy link
Contributor Author

@pevner-p2 I wasn't that much experienced with the project, and I also forked a fork of the original project (As of right now, that fork is 21 commits ahead of the original).
So it might not have the best formatting and/or the most reliable way that might serve everybody.

Worked for me at least, so I hope it helps more people haha

@JUSTFUN0368
Copy link

Since you guys are working on migrating to the new API version, you should adjust the get_board_items_query() and get_items_by_group_query() to nest items within the items_page object.

chdastolfo
chdastolfo previously approved these changes Nov 11, 2023
Copy link
Collaborator

@chdastolfo chdastolfo left a comment

Choose a reason for hiding this comment

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

👍 @pevner-p2 I agree. Would like to get these in to solve the immediate issue and then go back and do some cleanup and make things agree with the linter. I have to do some work to re-acquaint myself to this and don't want to be a blocker.

@chdastolfo
Copy link
Collaborator

Since you guys are working on migrating to the new API version, you should adjust the get_board_items_query() and get_items_by_group_query() to nest items within the items_page object.

I haven't used Monday in a little bit. Don't want to block this PR, but agree this should be done. Would you mind filing an issue for this?


return query

def duplicate_board_query(board_id: int, duplicate_type: DuplicateTypes):
Copy link
Collaborator

@chdastolfo chdastolfo Nov 11, 2023

Choose a reason for hiding this comment

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

Why does this have the same name as the method above? It's going to override the above function, as it's defined later in the file.

super().__init__(token, headers)

def create_column(self, board_id):
query = get_columns_by_board_query(board_id)
Copy link
Collaborator

@chdastolfo chdastolfo Nov 11, 2023

Choose a reason for hiding this comment

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

This seems like the wrong query. Shouldn't this be calling create_column from inside the query file?

@chdastolfo chdastolfo dismissed their stale review November 11, 2023 19:06

Found some blocking issues.

if self.token is not None:
headers[self.headername] = '{}'.format(self.token)

if variables is None:
headers['Content-Type'] = 'application/json'
if 'Content-Type' not in headers:
headers['Content-Type'] = 'application/json'
Copy link

Choose a reason for hiding this comment

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

Thanks for the great work @willnaoosmith and very happy to see it was picked up by the authors so quickly. Just a comment:

Mutating the headers seems like a bad idea; headers are not copied at any point in time in this PR, so they are shared across all Resources.

client = MondayClient(token = "abc",headers = {'API-Version' : '2023-10'})
client.items.client.headers # {'API-Version': '2023-10'}
client.boards.fetch_columns_by_board_id("lala") # Note this is on client.boards not client.items
client.items.client.headers 
""" Output {'API-Version': '2023-10',
 'Authorization': 'abc',
 'Content-Type': 'application/json'}
"""

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good catch. This was caught by our internal linter, so I'd glossed over it as a syntactic issue, but this is definitely a genuine bug.

Copy link

@matiboy matiboy left a comment

Choose a reason for hiding this comment

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

Thanks for the great work @willnaoosmith and very happy to see it was picked up by the authors so quickly.

There are however some issues that would need to be addressed. I hope you consider the option of setting api_version in constructor rather than passing headers, any maybe @pevner-p2 and @chdastolfo can weigh in on that option.

Thanks and sorry for the "long" review



class MondayClient:
def __init__(self, token):
def __init__(self, token, headers={}):
Copy link

Choose a reason for hiding this comment

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

An alternative would be to pass the desired version api_version="2023-10" to the constructor

Happy to hear your thoughts but here's why I think it'd be better:

  1. It makes it more obvious what we're trying to achieve with this param, since that's the only (current) logical use for the headers, outside of internal stuff like Content-Type etc.
  2. By keeping the version on e.g. self.api_version, it would allow to keep the old stuff working if someone wants to use the old API for the time being. For example the added items_page() in queries could be added only if self.api_version == '2023-10' etc
  3. See my comment above about mutating a default class param. A string doesn't have that issue
  4. Users of the lib don't need to know what the headers look like (yes, it's very simple to document, but still)

board_name = board_name if board_name else ""
workspace_id = workspace_id if workspace_id else None
folder_id = folder_id if folder_id else None
keep_subscribers = keep_subscribers if keep_subscribers else False
Copy link

Choose a reason for hiding this comment

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

For the 4 lines above,

  1. workspace_id and folder_id are already defaulting to None
  2. why not set the defaults in the function definition board_name: str = "", keep_subscribers: bool = False ?
  3. where possible, I'd avoid None since the typing is then incorrect (int = None will not be well liked); as we know that real workspace_id and folder_id will never be "0" why not set that as "falsy" value? Guess that's more of a preference

Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. agree
  2. agree
  3. Type annotations should be int | None or Union[int, None]

Then bellow:

if workspace_id is not None:
    # do something

if folder_id is not None:
    # do something

)

if workspace_id:
params += """, workspace_id: %s"""
Copy link

Choose a reason for hiding this comment

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

The values are already merged in the lines above, so wouldn't this extra params remain unmerged (workspace_id is never used)


class ColumnsResource(BaseResource):
def __init__(self, token, headers):
super().__init__(token, headers)
Copy link

Choose a reason for hiding this comment

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

Unnecessary since no change from super

Copy link

@matiboy matiboy Nov 13, 2023

Choose a reason for hiding this comment

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

Though I notice this is likely because other Resource sub classes (from original code) have an unnecessary super().__init__ too; I think would be nice to remove but of course not so important

board_id: int, column_title: str, column_type: None, defaults: dict[str, any] = None, description: str = None
):
"""C```reate a new column by board ID

Copy link

Choose a reason for hiding this comment

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

Extra "```"

Return:
[Dict[str,Any]]: Column's Monday details after creation
"""
defaults = defaults if defaults else ""
Copy link

Choose a reason for hiding this comment

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

defaults is typed as a dict? Should it be an empty string if not provided?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@matiboy you're calling out the correct thing -- I think the reason we're able to get away with dict or str is because monday_json_string converts the dictionary into a string -- I do agree that for correctness, this should be:

defaults = defaults or {}

[Dict[str,Any]]: Column's Monday details after creation
"""
defaults = defaults if defaults else ""
description = description if description else ""
Copy link

Choose a reason for hiding this comment

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

Might as well declare "" as default value in function definition

@@ -167,6 +166,75 @@ def delete_item_query(item_id):
return query


# COLUMNS RESOURCE QUERIES
def create_column(
board_id: int, column_title: str, column_type: None, defaults: dict[str, any] = None, description: str = None
Copy link

@matiboy matiboy Nov 13, 2023

Choose a reason for hiding this comment

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

column_type typed as None is likely erroneous. ColumnTypes seems to be the correct type (from doc string below)

else:

query = """mutation{
create_column(board_id: %s, title: "%s", description: "%s", column_type: %s, defaults: %s) {
Copy link

@matiboy matiboy Nov 13, 2023

Choose a reason for hiding this comment

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

Just a warning as the column_type seems to have been meant to be typed to ColumnTypes, string interpolation of enums has changed somewhere in 3.10 I believe:

class A(Enum):
  ABC = "123"
"""enum value: %s""" % (A.ABC,) # wrong: 'From enum value: A.ABC'
"""enum value: %s""" % (A.ABC.value,) # correct: enum value: 123

files = [
('variables[file]', (variables['file'], open(variables['file'], 'rb')))
]

try:
response = requests.request("POST", self.endpoint, headers=headers, data=payload, files=files)
response.raise_for_status()
self._catch_error(response)
Copy link
Collaborator

@rhymiz rhymiz Nov 15, 2023

Choose a reason for hiding this comment

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

You're not technically "catching" any errors here, you're checking for the present of any errors, so we should probably rename the method to _raise_if_error()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants