-
Notifications
You must be signed in to change notification settings - Fork 833
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
Add files.upload v2 support #1272
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1272 +/- ##
==========================================
- Coverage 86.07% 85.70% -0.38%
==========================================
Files 84 84
Lines 7779 7826 +47
==========================================
+ Hits 6696 6707 +11
- Misses 1083 1119 +36
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comments for reviewers
response = client.files_upload( | ||
channels="C3UKJTQAC", | ||
file="files.pdf", | ||
response = client.files_upload_v2( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't yet generated the page. We will update the live site once we release a new version including the change.
@@ -242,7 +242,7 @@ async def test_uploading_binary_files_async(self): | |||
) | |||
self.assertIsNotNone(upload) | |||
|
|||
deletion = client.files_delete(file=upload["file"]["id"]) | |||
deletion = await client.files_delete(file=upload["file"]["id"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just improvements to existing test code
@@ -2952,7 +2955,7 @@ async def files_upload( | |||
self, | |||
*, | |||
file: Optional[Union[str, bytes, IOBase]] = None, | |||
content: Optional[str] = None, | |||
content: Optional[Union[str, bytes]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While touching the code this time, I noticed that this type hint has been wrong
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 💯
slack_sdk/web/async_client.py
Outdated
# To upload multiple files at a time | ||
upload_files: Optional[List[Dict[str, Any]]] = None, | ||
channel: Optional[str] = None, | ||
channels: Optional[Union[str, Sequence[str]]] = None, # having n channels is no longer supported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intentionally didn't remove this keyword argument. Unlike the Java SDK, there is no compiler detection in Python. Thus, removing the argument name that existed in files.upload (v1) can result in many developers' confusion.
That being said, sharing files in multiple channels is no longer supported. So, I've added runtime validation below to inform the breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we should go ahead and remove this parameter in order to make it unambiguous to the developer while they are writing code that sharing files with multiple channels is not supported by this method.
Maybe something as follows could be implemented to support runtime validation, without including the parameter in the method definition
if 'channels' in kwargs:
raise e.SlackRequestError("Sharing files with multiple channels is no longer supported, share files in each channel separately")
let me know what you think of this approach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamBergamin Thanks. I think it's fine to move this argument to kwargs, but I still would like to support it as long as the list has only one item. I am pretty sure that most of the existing code has only a single channel in this parameter. Perhaps, we can add a warning logging even for the single item use case, but we can allow it for smoother migration.
slack_sdk/web/async_client.py
Outdated
(isinstance(channels, (list, Tuple)) and len(channels) > 1) | ||
or (isinstance(channels, str) and len(channels.split(",")) > 1) | ||
): | ||
raise e.SlackRequestError("Sharing files with multiple channels is no longer supported") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added runtime validation for the unsupported pattern
slack_sdk/web/async_client.py
Outdated
): | ||
raise e.SlackRequestError("Sharing files with multiple channels is no longer supported") | ||
if filetype is not None: | ||
warnings.warn("filetype is no longer supported. Please remove it from the arguments.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hesitated to raise an exception for this pattern but if others strongly believe that we should do, I'm fine to change this. But I don't think this can be so beneficial particularly for the devs who migrate existing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe kwargs
validation could be used here instead of including filetype
as a parameter in the method definition, but let me know what you think
if 'filetype' in kwargs:
warnings.warn("filetype is no longer supported. Please remove it from the arguments.")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, this is fine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really good and slick work 💯 I left a few ideas let me know what you think
@@ -2952,7 +2955,7 @@ async def files_upload( | |||
self, | |||
*, | |||
file: Optional[Union[str, bytes, IOBase]] = None, | |||
content: Optional[str] = None, | |||
content: Optional[Union[str, bytes]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 💯
slack_sdk/web/async_client.py
Outdated
# To upload multiple files at a time | ||
upload_files: Optional[List[Dict[str, Any]]] = None, | ||
channel: Optional[str] = None, | ||
channels: Optional[Union[str, Sequence[str]]] = None, # having n channels is no longer supported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we should go ahead and remove this parameter in order to make it unambiguous to the developer while they are writing code that sharing files with multiple channels is not supported by this method.
Maybe something as follows could be implemented to support runtime validation, without including the parameter in the method definition
if 'channels' in kwargs:
raise e.SlackRequestError("Sharing files with multiple channels is no longer supported, share files in each channel separately")
let me know what you think of this approach
slack_sdk/web/async_client.py
Outdated
): | ||
raise e.SlackRequestError("Sharing files with multiple channels is no longer supported") | ||
if filetype is not None: | ||
warnings.warn("filetype is no longer supported. Please remove it from the arguments.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe kwargs
validation could be used here instead of including filetype
as a parameter in the method definition, but let me know what you think
if 'filetype' in kwargs:
warnings.warn("filetype is no longer supported. Please remove it from the arguments.")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will update the code shortly
slack_sdk/web/async_client.py
Outdated
# To upload multiple files at a time | ||
upload_files: Optional[List[Dict[str, Any]]] = None, | ||
channel: Optional[str] = None, | ||
channels: Optional[Union[str, Sequence[str]]] = None, # having n channels is no longer supported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamBergamin Thanks. I think it's fine to move this argument to kwargs, but I still would like to support it as long as the list has only one item. I am pretty sure that most of the existing code has only a single channel in this parameter. Perhaps, we can add a warning logging even for the single item use case, but we can allow it for smoother migration.
slack_sdk/web/async_client.py
Outdated
): | ||
raise e.SlackRequestError("Sharing files with multiple channels is no longer supported") | ||
if filetype is not None: | ||
warnings.warn("filetype is no longer supported. Please remove it from the arguments.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, this is fine
da0af3e
to
a30f930
Compare
@@ -3051,8 +3060,8 @@ def files_upload_v2( | |||
token=kwargs.get("token"), | |||
) | |||
_validate_for_legacy_client(url_response) | |||
f["file_id"] = url_response.get("file_id") | |||
f["upload_url"] = url_response.get("upload_url") | |||
f["file_id"] = url_response.get("file_id") # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was unintentionally inconsistent
@WilliamBergamin I've updated the code and it's now ready for reviews again: a30f930 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me 🚀
slack_sdk/web/async_client.py
Outdated
alt_txt: Optional[str] = None, | ||
snippet_type: Optional[str] = None, | ||
# To upload multiple files at a time | ||
upload_files: Optional[List[Dict[str, Any]]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Upload is already in the method name, would it make sense to call this arg multiple_files
or files
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@srajiang Good point. The reason why I hesitated to name this as files
is that we have file
for the single file upload and the meaning of file
is quite different. file
is a file content and it does not include its metadata such as alt_txt and so on. Thus, naming the list of dict objects as files
can sound inconsistent. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps just switching the parts around to file_uploads
then? More suggestive that there's content and possibly metadata as well and still very clear that it's plural and different from file
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@srajiang This makes sense! I've renamed the argument ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, renamed the same part in Java SDK PR: slackapi/java-slack-sdk#1065
slack_sdk/web/async_client.py
Outdated
@@ -21,6 +22,10 @@ | |||
_update_call_participants, | |||
_warn_if_text_or_attachment_fallback_is_missing, | |||
_remove_none_values, | |||
_to_v2_upload_file_item, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seratch - very minor but this could then be named to _to_v2_file_upload_item for consistency
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@srajiang Thanks for catching this, renamed ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thank you :)
@srajiang Sounds great. I like the warning for files.upload! I will add it before merging this. However, I hesitate to add the ones for new endpoints. Some of developers may be using it intentionally for some reason (e.g., to take full control, need more detailed logging, and so on). Probably, we will receive a request to add some option to remove it. If we add the informative logging output, the log level may should be info/debug instead. |
*, | ||
# for sending a single file | ||
filename: Optional[str] = None, # you can skip this only when sending along with content parameter | ||
file: Optional[Union[str, bytes, IOBase]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seratch - is this type hint correct? I tried to pass in a io.BytesIO
object here as the file
as I thought IOBase
covers that as well as a real file handle (e.g. from open("filename", "rb")
) - but it failed with TypeError: object of type '_io.BytesIO' has no len()
- len comes from _to_v2_file_upload_item
I believe. So either the type hint is incorrect or the implementation of _to_v2_file_upload_item
needs tweaking? You could probably just do something like this?
def _to_v2_file_upload_item(upload_file: Dict[str, Any]) -> Dict[str, Optional[Any]]:
file = upload_file.get("file")
content = upload_file.get("content")
data: Optional[bytes] = None
if file is not None:
if isinstance(file, str): # filepath
with open(file.encode("utf-8", "ignore"), "rb") as readable:
data = readable.read()
elif isinstance(file, io.IOBase):
# new stuff
data = file.read()
if isinstance(data, str):
data = data.encode()
elif isinstance(file, bytes):
data = file
else:
raise SlackRequestError("The given file must be either a filepath as str or data as bytes/IOBase")
elif content is not None:
if isinstance(content, str): # str content
data = content.encode("utf-8")
elif isinstance(content, bytes):
data = content
else:
raise SlackRequestError("The given content given as str or as bytes")
filename = upload_file.get("filename")
if upload_file.get("filename") is None and isinstance(file, str):
# use the local filename if filename is missing
if upload_file.get("filename") is None:
filename = file.split(os.path.sep)[-1]
else:
filename = "Uploaded file"
title = upload_file.get("title", "Uploaded file")
if data is None:
raise SlackRequestError(f"File content not found for filename: {filename}, title: {title}")
if title is None:
title = filename # to be consistent with files.upload API
return {
"filename": filename,
"data": data,
"length": len(data),
"title": title,
"alt_txt": upload_file.get("alt_txt"),
"snippet_type": upload_file.get("snippet_type"),
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Alex-ley-scrub Thanks for flagging this! This should be improved. We will look into this later.
Summary
This pull request adds a new way to upload files to Slack.
The legacy files.upload API endpoint now has severe performance issues as described in the following reports:
Slack platform team decided to unlock a new way to upload files using the following endpoints:
https://files.slack.com/upload/v1/***
to upload the file content per fileThis pull request adds supports for the new endpoints, so that now the low-level APIs are available for developers. Having said that, going through the above process for uploading files requires many lines of code on 3rd party app side. Also, following all the steps can be confusing for developers.
For this reason, in addition to the low-level API supports, I propose to add WebClient#files_upload_v2 method as a wrapper of the whole file-upload operation. Here are the code examples demonstrating how it works:
see also:
Category (place an
x
in each of the[ ]
)/docs-src
(Documents, have you run./scripts/docs.sh
?)/docs-src-v2
(Documents, have you run./scripts/docs-v2.sh
?)/tutorial
(PythOnBoardingBot tutorial)tests
/integration_tests
(Automated tests for this library)Requirements (place an
x
in each[ ]
)python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh
after making the changes.