From 5066e21ce210b9ca1e1fc7a592f564f9730e6feb Mon Sep 17 00:00:00 2001 From: Elyse Date: Tue, 2 Apr 2024 11:57:22 -0500 Subject: [PATCH 01/11] initial commit --- parsons/google/google_slides.py | 183 ++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 parsons/google/google_slides.py diff --git a/parsons/google/google_slides.py b/parsons/google/google_slides.py new file mode 100644 index 0000000000..1b2aa08504 --- /dev/null +++ b/parsons/google/google_slides.py @@ -0,0 +1,183 @@ +import os +import json +import logging + +from parsons.etl.table import Table +from parsons.google.utitities import setup_google_application_credentials +from parsons.tools.credential_tools import decode_credential +from google.oauth2 import service_account +from googleapiclient.discovery import build + +logger = logging.getLogger(__name__) + +class GoogleSlides: + """ + A connector for Google Slides, handling slide creation. + + `Args:` + google_keyfile_dict: dict + A dictionary of Google Drive API credentials, parsed from JSON provided + by the Google Developer Console. Required if env variable + ``GOOGLE_DRIVE_CREDENTIALS`` is not populated. + subject: string + In order to use account impersonation, pass in the email address of the account to be + impersonated as a string. + """ + + def __init__(self, google_keyfile_dict=None, subject=None): + + scope = [ + "https://www.googleapis.com/auth/presentations", + "https://www.googleapis.com/auth/drive", + ] + + setup_google_application_credentials( + google_keyfile_dict, "GOOGLE_DRIVE_CREDENTIALS" + ) + google_credential_file = open(os.environ["GOOGLE_DRIVE_CREDENTIALS"]) + credentials_dict = json.load(google_credential_file) + + credentials = Credentials.from_service_account_info( + credentials_dict, scopes=scope, subject=subject + ) + + self.gsheets_client = build('slides', 'v1', credentials=credentials) + + + def create_presentation(self, title): + """ + `Args:` + title: str + this is the title you'd like to give your presentation + `Returns:` + the presentation object + """ + + body = {"title": title} + presentation = self.presentations().create(body=body).execute() + logger.info( + f"Created presentation with ID:" f"{(presentation.get('presentationId'))}" + ) + return presentation + + + def get_presentation(self, presentation_id): + """ + `Args:` + presentation_id: str + this is the ID of the presentation to put the duplicated slide + `Returns:` + the presentation object + """ + + presentation = ( + self.presentations().get(presentationId=presentation_id).execute() + ) + + return presentation + + + def get_slide(self, presentation_id, slide_number): + """ + `Args:` + presentation_id: str + this is the ID of the presentation to put the duplicated slide + slide_number: int + the slide number you are seeking + `Returns:` + the slide object + """ + + presentation = self.get_presentation(self, presentation_id) + slide = presentation["slides"][slide_number - 1] + + return slide + + + def duplicate_slide(self, source_slide_id, presentation_id): + """ + `Args:` + source_slide_id: str + this is the ID of the source slide to be duplicated + presentation_id: str + this is the ID of the presentation to put the duplicated slide + `Returns:` + the ID of the duplicated slide + """ + + batch_request = {"requests": [{"duplicateObject": {"objectId": source_slide_id}}]} + response = ( + self.presentations() + .batchUpdate(presentationId=presentation_id, body=batch_request) + .execute() + ) + + duplicated_slide_id = response["replies"][0]["duplicateObject"]["objectId"] + + return duplicated_slide_id + + + def replace_slide_text( + self, presentation_id, slide_id, original_text, replace_text + ): + """ + `Args:` + presentation_id: str + this is the ID of the presentation to put the duplicated slide + origianl_text: str + the text to be replaced + replace_text: str + the desired new text + `Returns:` + None + """ + + reqs = [ + { + "replaceAllText": { + "containsText": {"text": original_text}, + "replaceText": replace_text, + "pageObjectIds": [slide_id], + } + }, + ]s + + self.presentations().batchUpdate( + body={"requests": reqs}, presentationId=presentation_id + ).execute() + + return None + + + def replace_slide_image(self, presentation_id, slide, obj, img_url): + """ + `Args:` + presentation_id: str + this is the ID of the presentation to put the duplicated slide + slide: dict + the slide object + replace_text: str + the desired new text + `Returns:` + None + """ + + reqs = [ + { + "createImage": { + "url": img_url, + "elementProperties": { + "pageObjectId": slide["objectId"], + "size": obj["size"], + "transform": obj["transform"], + }, + } + }, + {"deleteObject": {"objectId": obj["objectId"]}}, + ] + + self.presentations().batchUpdate( + body={"requests": reqs}, presentationId=presentation_id + ).execute() + + return None \ No newline at end of file From baadc24e0eefac27df5fa82dc160e8bee33228f8 Mon Sep 17 00:00:00 2001 From: Elyse Date: Tue, 2 Apr 2024 14:41:22 -0500 Subject: [PATCH 02/11] working code --- .../github/parsons-fork/bin/Activate.ps1 | 241 ++++++++++++++++++ .../github/parsons-fork/bin/activate | 76 ++++++ .../github/parsons-fork/bin/activate.csh | 37 +++ .../github/parsons-fork/bin/activate.fish | 75 ++++++ .../github/parsons-fork/bin/easy_install | 8 + .../github/parsons-fork/bin/easy_install-3.8 | 8 + .../elyseweiss/github/parsons-fork/bin/pip | 8 + .../elyseweiss/github/parsons-fork/bin/pip3 | 8 + .../elyseweiss/github/parsons-fork/bin/pip3.8 | 8 + .../elyseweiss/github/parsons-fork/bin/python | 1 + .../github/parsons-fork/bin/python3 | 1 + .../elyseweiss/github/parsons-fork/pyvenv.cfg | 3 + parsons/google/google_drive.py | 107 ++++++++ parsons/google/google_slides.py | 93 ++++--- 14 files changed, 644 insertions(+), 30 deletions(-) create mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 create mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/activate create mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/activate.csh create mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/activate.fish create mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/easy_install create mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 create mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/pip create mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/pip3 create mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/pip3.8 create mode 120000 $/Users/elyseweiss/github/parsons-fork/bin/python create mode 120000 $/Users/elyseweiss/github/parsons-fork/bin/python3 create mode 100644 $/Users/elyseweiss/github/parsons-fork/pyvenv.cfg create mode 100644 parsons/google/google_drive.py diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 b/$/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 new file mode 100644 index 0000000000..2fb3852c3c --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 @@ -0,0 +1,241 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/activate b/$/Users/elyseweiss/github/parsons-fork/bin/activate new file mode 100644 index 0000000000..7e0dc0d3e8 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/activate @@ -0,0 +1,76 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + if [ "x(parsons-fork) " != x ] ; then + PS1="(parsons-fork) ${PS1:-}" + else + if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then + # special case for Aspen magic directories + # see https://aspen.io/ + PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" + else + PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" + fi + fi + export PS1 +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r +fi diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/activate.csh b/$/Users/elyseweiss/github/parsons-fork/bin/activate.csh new file mode 100644 index 0000000000..83dfa1795f --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/activate.csh @@ -0,0 +1,37 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + if ("parsons-fork" != "") then + set env_name = "parsons-fork" + else + if (`basename "VIRTUAL_ENV"` == "__") then + # special case for Aspen magic directories + # see https://aspen.io/ + set env_name = `basename \`dirname "$VIRTUAL_ENV"\`` + else + set env_name = `basename "$VIRTUAL_ENV"` + endif + endif + set prompt = "[$env_name] $prompt" + unset env_name +endif + +alias pydoc python -m pydoc + +rehash diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/activate.fish b/$/Users/elyseweiss/github/parsons-fork/bin/activate.fish new file mode 100644 index 0000000000..8ecf90ace6 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/activate.fish @@ -0,0 +1,75 @@ +# This file must be used with ". bin/activate.fish" *from fish* (http://fishshell.org) +# you cannot run it directly + +function deactivate -d "Exit virtualenv and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + functions -e fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + + set -e VIRTUAL_ENV + if test "$argv[1]" != "nondestructive" + # Self destruct! + functions -e deactivate + end +end + +# unset irrelevant variables +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# unset PYTHONHOME if set +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # save the current fish_prompt function as the function _old_fish_prompt + functions -c fish_prompt _old_fish_prompt + + # with the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command + set -l old_status $status + + # Prompt override? + if test -n "(parsons-fork) " + printf "%s%s" "(parsons-fork) " (set_color normal) + else + # ...Otherwise, prepend env + set -l _checkbase (basename "$VIRTUAL_ENV") + if test $_checkbase = "__" + # special case for Aspen magic directories + # see https://aspen.io/ + printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal) + else + printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal) + end + end + + # Restore the return status of the previous command. + echo "exit $old_status" | . + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" +end diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/easy_install b/$/Users/elyseweiss/github/parsons-fork/bin/easy_install new file mode 100755 index 0000000000..1e296a0ab5 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/easy_install @@ -0,0 +1,8 @@ +#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from setuptools.command.easy_install import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 b/$/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 new file mode 100755 index 0000000000..1e296a0ab5 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 @@ -0,0 +1,8 @@ +#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from setuptools.command.easy_install import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/pip b/$/Users/elyseweiss/github/parsons-fork/bin/pip new file mode 100755 index 0000000000..ee95acbfe6 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/pip @@ -0,0 +1,8 @@ +#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/pip3 b/$/Users/elyseweiss/github/parsons-fork/bin/pip3 new file mode 100755 index 0000000000..ee95acbfe6 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/pip3.8 b/$/Users/elyseweiss/github/parsons-fork/bin/pip3.8 new file mode 100755 index 0000000000..ee95acbfe6 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/pip3.8 @@ -0,0 +1,8 @@ +#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/python b/$/Users/elyseweiss/github/parsons-fork/bin/python new file mode 120000 index 0000000000..20e95ae78d --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/python @@ -0,0 +1 @@ +/Users/elyseweiss/Environments/parsons/bin/python \ No newline at end of file diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/python3 b/$/Users/elyseweiss/github/parsons-fork/bin/python3 new file mode 120000 index 0000000000..d8654aa0e2 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/bin/python3 @@ -0,0 +1 @@ +python \ No newline at end of file diff --git a/$/Users/elyseweiss/github/parsons-fork/pyvenv.cfg b/$/Users/elyseweiss/github/parsons-fork/pyvenv.cfg new file mode 100644 index 0000000000..3467eb3ba9 --- /dev/null +++ b/$/Users/elyseweiss/github/parsons-fork/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /Users/elyseweiss/Environments/parsons/bin +include-system-site-packages = false +version = 3.8.6 diff --git a/parsons/google/google_drive.py b/parsons/google/google_drive.py new file mode 100644 index 0000000000..86ed62a98f --- /dev/null +++ b/parsons/google/google_drive.py @@ -0,0 +1,107 @@ +import os +import json +import logging + +from parsons.etl.table import Table +from parsons.google.utitities import setup_google_application_credentials +from parsons.tools.credential_tools import decode_credential +from google.oauth2 import service_account +from googleapiclient.discovery import build +from google.oauth2.service_account import Credentials + +logger = logging.getLogger(__name__) + +class GoogleDrive: + """ + A connector for Google Drive, largely handling permissions. + + `Args:` + google_keyfile_dict: dict + A dictionary of Google Drive API credentials, parsed from JSON provided + by the Google Developer Console. Required if env variable + ``GOOGLE_DRIVE_CREDENTIALS`` is not populated. + subject: string + In order to use account impersonation, pass in the email address of the account to be + impersonated as a string. + """ + + def __init__(self, google_keyfile_dict=None, subject=None): + + scope = ['https://www.googleapis.com/auth/drive'] + + setup_google_application_credentials( + google_keyfile_dict, "GOOGLE_DRIVE_CREDENTIALS" + ) + google_credential_file = open(os.environ["GOOGLE_DRIVE_CREDENTIALS"]) + credentials_dict = json.load(google_credential_file) + + credentials = Credentials.from_service_account_info( + credentials_dict, scopes=scope, subject=subject + ) + + self.client = build('drive', 'v3', credentials=credentials) + + + def _share_object(self, file_id, permission_dict): + + # Send the request to share the file + self.client.permissions().create(fileId=file_id, body=permission_dict).execute() + + + def share_object(self, file_id, email_addresses, role='reader', type='user'): + """ + `Args:` + file_id: str + this is the ID of the object you are hoping to share + email_addresses: list + this is the list of the email addresses you want to share; + this can be set to `None` if you choose `type='anyone'` + role: str + Options are -- owner, organizer, fileOrganizer, writer, commenter, reader + https://developers.google.com/drive/api/guides/ref-roles + type: str + Options are -- user, group, domain, anyone + `Returns:` + None + """ + + permissions = [{ + 'type': type, + 'role': role, + 'emailAddress': email + } for email in email_addresses] + + for permission in permissions: + self._share_object(file_id, permission) + + + def transfer_ownership(self, file_id, new_owner_email): + """ + `Args:` + file_id: str + this is the ID of the object you are hoping to share + new_owner_email: str + the email address of the intended new owner + `Returns:` + None + """ + permissions = self.client.permissions().list(fileId=file_id).execute() + + # Find the current owner + current_owner_permission = next((p for p in permissions.get('permissions', []) if 'owner' in p), None) + + if current_owner_permission: + # Update the permission to transfer ownership + new_owner_permission = { + 'type': 'user', + 'role': 'owner', + 'emailAddress': new_owner_email + } + self.client.permissions().update(fileId=file_id, + permissionId=current_owner_permission['id'], + body=new_owner_permission).execute() + logger.info(f"Ownership transferred successfully to {new_owner_email}.") + else: + logger.info("File does not have a current owner.") + + \ No newline at end of file diff --git a/parsons/google/google_slides.py b/parsons/google/google_slides.py index 1b2aa08504..5eb89c55e4 100644 --- a/parsons/google/google_slides.py +++ b/parsons/google/google_slides.py @@ -7,6 +7,7 @@ from parsons.tools.credential_tools import decode_credential from google.oauth2 import service_account from googleapiclient.discovery import build +from google.oauth2.service_account import Credentials logger = logging.getLogger(__name__) @@ -41,7 +42,7 @@ def __init__(self, google_keyfile_dict=None, subject=None): credentials_dict, scopes=scope, subject=subject ) - self.gsheets_client = build('slides', 'v1', credentials=credentials) + self.client = build('slides', 'v1', credentials=credentials) def create_presentation(self, title): @@ -54,7 +55,7 @@ def create_presentation(self, title): """ body = {"title": title} - presentation = self.presentations().create(body=body).execute() + presentation = self.client.presentations().create(body=body).execute() logger.info( f"Created presentation with ID:" f"{(presentation.get('presentationId'))}" ) @@ -71,7 +72,7 @@ def get_presentation(self, presentation_id): """ presentation = ( - self.presentations().get(presentationId=presentation_id).execute() + self.client.presentations().get(presentationId=presentation_id).execute() ) return presentation @@ -88,26 +89,28 @@ def get_slide(self, presentation_id, slide_number): the slide object """ - presentation = self.get_presentation(self, presentation_id) + presentation = self.get_presentation(presentation_id) slide = presentation["slides"][slide_number - 1] return slide - def duplicate_slide(self, source_slide_id, presentation_id): + def duplicate_slide(self, source_slide_number, presentation_id): """ `Args:` - source_slide_id: str - this is the ID of the source slide to be duplicated + source_slide_number: int + this should reflect the slide # (e.g. 2 = 2nd slide) presentation_id: str this is the ID of the presentation to put the duplicated slide `Returns:` the ID of the duplicated slide """ + source_slide = self.get_slide(presentation_id, source_slide_number) + source_slide_id = source_slide['objectId'] batch_request = {"requests": [{"duplicateObject": {"objectId": source_slide_id}}]} response = ( - self.presentations() + self.client.presentations() .batchUpdate(presentationId=presentation_id, body=batch_request) .execute() ) @@ -118,12 +121,14 @@ def duplicate_slide(self, source_slide_id, presentation_id): def replace_slide_text( - self, presentation_id, slide_id, original_text, replace_text + self, presentation_id, slide_number, original_text, replace_text ): """ `Args:` presentation_id: str this is the ID of the presentation to put the duplicated slide + slide_number: int + this should reflect the slide # (e.g. 2 = 2nd slide) origianl_text: str the text to be replaced replace_text: str @@ -132,6 +137,9 @@ def replace_slide_text( None """ + slide = self.get_slide(presentation_id, slide_number) + slide_id = slide['objectId'] + reqs = [ { "replaceAllText": { @@ -140,43 +148,68 @@ def replace_slide_text( "pageObjectIds": [slide_id], } }, - ]s + ] - self.presentations().batchUpdate( + self.client.presentations().batchUpdate( body={"requests": reqs}, presentationId=presentation_id ).execute() return None - def replace_slide_image(self, presentation_id, slide, obj, img_url): + def get_slide_images(self, presentation_id, slide_number): """ `Args:` presentation_id: str this is the ID of the presentation to put the duplicated slide - slide: dict - the slide object - replace_text: str - the desired new text + slide_number: int + this should reflect the slide # (e.g. 2 = 2nd slide) + `Returns:` + a list of object dicts for image objects + """ + + slide = self.get_slide(presentation_id, slide_number) + + images = [] + for x in slide['pageElements']: + if 'image' in x.keys(): + images.append(x) + + return images + + + def replace_slide_image(self, presentation_id, slide_number, image_obj, new_image_url): + """ + `Args:` + presentation_id: str + this is the ID of the presentation to put the duplicated slide + slide_number: int + this should reflect the slide # (e.g. 2 = 2nd slide) + image_obj: dict + the image object -- can use `get_slide_images()` + new_image_url: str + the url that contains the desired image `Returns:` None """ - reqs = [ - { - "createImage": { - "url": img_url, - "elementProperties": { - "pageObjectId": slide["objectId"], - "size": obj["size"], - "transform": obj["transform"], - }, - } - }, - {"deleteObject": {"objectId": obj["objectId"]}}, - ] + slide = self.get_slide(presentation_id, slide_number) - self.presentations().batchUpdate( + reqs = [ + { + "createImage": { + "url": new_image_url, + "elementProperties": { + "pageObjectId": slide["objectId"], + "size": image_obj["size"], + "transform": image_obj["transform"], + }, + } + }, + {"deleteObject": {"objectId": image_obj["objectId"]}}, + ] + + self.client.presentations().batchUpdate( body={"requests": reqs}, presentationId=presentation_id ).execute() From 8e1de172c30a1ada3f7e73f7759c9e93890594de Mon Sep 17 00:00:00 2001 From: Elyse Date: Wed, 3 Apr 2024 14:56:47 -0500 Subject: [PATCH 03/11] tests and more --- .gitignore | 1 + parsons/__init__.py | 2 + parsons/google/google_drive.py | 63 +++++++++++++++----- parsons/google/google_slides.py | 59 ++++++++++-------- test/test_google/test_google_drive.py | 35 +++++++++++ test/test_google/test_google_slides.py | 82 ++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 41 deletions(-) create mode 100644 test/test_google/test_google_drive.py create mode 100644 test/test_google/test_google_slides.py diff --git a/.gitignore b/.gitignore index 8d6520e18e..a55f944878 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,4 @@ bill_com_credentials.* docs/html docs/dirhtml *.sw* +settings.json diff --git a/parsons/__init__.py b/parsons/__init__.py index 66ebf85d47..b5322d9257 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -65,7 +65,9 @@ ("parsons.google.google_bigquery", "GoogleBigQuery"), ("parsons.google.google_civic", "GoogleCivic"), ("parsons.google.google_cloud_storage", "GoogleCloudStorage"), + ("parsons.google.google_drive", "GoogleDrive"), ("parsons.google.google_sheets", "GoogleSheets"), + ("parsons.google.google_slides", "GoogleSlides"), ("parsons.hustle.hustle", "Hustle"), ("parsons.mailchimp.mailchimp", "Mailchimp"), ("parsons.mobilecommons.mobilecommons", "MobileCommons"), diff --git a/parsons/google/google_drive.py b/parsons/google/google_drive.py index 86ed62a98f..b9b2cb147b 100644 --- a/parsons/google/google_drive.py +++ b/parsons/google/google_drive.py @@ -2,15 +2,13 @@ import json import logging -from parsons.etl.table import Table from parsons.google.utitities import setup_google_application_credentials -from parsons.tools.credential_tools import decode_credential -from google.oauth2 import service_account from googleapiclient.discovery import build from google.oauth2.service_account import Credentials logger = logging.getLogger(__name__) + class GoogleDrive: """ A connector for Google Drive, largely handling permissions. @@ -41,40 +39,71 @@ def __init__(self, google_keyfile_dict=None, subject=None): self.client = build('drive', 'v3', credentials=credentials) - + def get_permissions(self, file_id): + """ + `Args:` + file_id: str + this is the ID of the object you are hoping to share + `Returns:` + permission dict + """ + + p = self.client.permissions().list(fileId=file_id).execute() + + return p + def _share_object(self, file_id, permission_dict): # Send the request to share the file - self.client.permissions().create(fileId=file_id, body=permission_dict).execute() + p = self.client.permissions().create(fileId=file_id, body=permission_dict).execute() + return p - def share_object(self, file_id, email_addresses, role='reader', type='user'): + def share_object(self, file_id, email_addresses=None, role='reader', type='user'): """ `Args:` file_id: str this is the ID of the object you are hoping to share email_addresses: list this is the list of the email addresses you want to share; - this can be set to `None` if you choose `type='anyone'` + set to a list of domains like `['domain']` if you choose `type='domain'`; + set to `None` if you choose `type='anyone'` role: str Options are -- owner, organizer, fileOrganizer, writer, commenter, reader https://developers.google.com/drive/api/guides/ref-roles type: str Options are -- user, group, domain, anyone `Returns:` - None + List of permission objects """ + if role not in ['owner', 'organizer', 'fileOrganizer', 'writer', 'commenter', 'reader']: + raise Exception(f"{role} not from the allowed list of: \ + owner, organizer, fileOrganizer, writer, commenter, reader") + + if type not in ['user', 'group', 'domain', 'anyone']: + raise Exception(f"{type} not from the allowed list of: \ + user, group, domain, anyone") + + if type=='domain': + permissions = [{ + 'type': type, + 'role': role, + 'domain': email + } for email in email_addresses] + else: + permissions = [{ + 'type': type, + 'role': role, + 'emailAddress': email + } for email in email_addresses] - permissions = [{ - 'type': type, - 'role': role, - 'emailAddress': email - } for email in email_addresses] - + new_permissions = [] for permission in permissions: - self._share_object(file_id, permission) + p = self._share_object(file_id, permission) + new_permissions.append(p) + + return new_permissions - def transfer_ownership(self, file_id, new_owner_email): """ `Args:` @@ -104,4 +133,6 @@ def transfer_ownership(self, file_id, new_owner_email): else: logger.info("File does not have a current owner.") + return None + \ No newline at end of file diff --git a/parsons/google/google_slides.py b/parsons/google/google_slides.py index 5eb89c55e4..98d67de320 100644 --- a/parsons/google/google_slides.py +++ b/parsons/google/google_slides.py @@ -2,15 +2,13 @@ import json import logging -from parsons.etl.table import Table from parsons.google.utitities import setup_google_application_credentials -from parsons.tools.credential_tools import decode_credential -from google.oauth2 import service_account from googleapiclient.discovery import build from google.oauth2.service_account import Credentials logger = logging.getLogger(__name__) + class GoogleSlides: """ A connector for Google Slides, handling slide creation. @@ -44,7 +42,6 @@ def __init__(self, google_keyfile_dict=None, subject=None): self.client = build('slides', 'v1', credentials=credentials) - def create_presentation(self, title): """ `Args:` @@ -61,7 +58,6 @@ def create_presentation(self, title): ) return presentation - def get_presentation(self, presentation_id): """ `Args:` @@ -77,6 +73,29 @@ def get_presentation(self, presentation_id): return presentation + def duplicate_slide(self, presentation_id, source_slide_number): + """ + `Args:` + presentation_id: str + this is the ID of the presentation to put the duplicated slide + source_slide_number: int + this should reflect the slide # (e.g. 2 = 2nd slide) + `Returns:` + the duplicated slide object + """ + source_slide = self.get_slide(presentation_id, source_slide_number) + source_slide_id = source_slide['objectId'] + + batch_request = {"requests": [{"duplicateObject": {"objectId": source_slide_id}}]} + response = ( + self.client.presentations() + .batchUpdate(presentationId=presentation_id, body=batch_request) + .execute() + ) + + duplicated_slide = response["replies"][0]["duplicateObject"] + + return duplicated_slide def get_slide(self, presentation_id, slide_number): """ @@ -94,31 +113,23 @@ def get_slide(self, presentation_id, slide_number): return slide - - def duplicate_slide(self, source_slide_number, presentation_id): + def delete_slide(self, presentation_id, slide_number): """ `Args:` - source_slide_number: int - this should reflect the slide # (e.g. 2 = 2nd slide) presentation_id: str this is the ID of the presentation to put the duplicated slide + slide_number: int + the slide number you are seeking `Returns:` - the ID of the duplicated slide + None """ - source_slide = self.get_slide(presentation_id, source_slide_number) - source_slide_id = source_slide['objectId'] - - batch_request = {"requests": [{"duplicateObject": {"objectId": source_slide_id}}]} - response = ( - self.client.presentations() - .batchUpdate(presentationId=presentation_id, body=batch_request) - .execute() - ) - - duplicated_slide_id = response["replies"][0]["duplicateObject"]["objectId"] - - return duplicated_slide_id + slide = self.get_slide(presentation_id, slide_number) + self.client.presentations().pages().delete( + presentationId=presentation_id, + pageObjectId=slide['objectId'] + ).execute() + return None def replace_slide_text( self, presentation_id, slide_number, original_text, replace_text @@ -156,7 +167,6 @@ def replace_slide_text( return None - def get_slide_images(self, presentation_id, slide_number): """ `Args:` @@ -177,7 +187,6 @@ def get_slide_images(self, presentation_id, slide_number): return images - def replace_slide_image(self, presentation_id, slide_number, image_obj, new_image_url): """ `Args:` diff --git a/test/test_google/test_google_drive.py b/test/test_google/test_google_drive.py new file mode 100644 index 0000000000..5cd94995c8 --- /dev/null +++ b/test/test_google/test_google_drive.py @@ -0,0 +1,35 @@ +import unittest +import os +import json +import logging + +from parsons.etl.table import Table +from parsons.google.utitities import setup_google_application_credentials +from parsons.tools.credential_tools import decode_credential +from google.oauth2 import service_account +from googleapiclient.discovery import build +from google.oauth2.service_account import Credentials + +# Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc + +@unittest.skipIf( + not os.environ.get("LIVE_TEST"), "Skipping because not running live test" +) + +def setUp(self): + + self.gd = GoogleDrive() + +def test_get_permissions(self): + + file_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + p = self.gd.get_permissinos(file_id) + self.assertTrue(True, 'anyoneWithLink' in [x['id'] for x in p['permissions']]) + +def test_share_object(self): + + file_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + email_addresses=["bob@bob.com"] + + shared = self.gd.share_object(file_id, email_addresses) + self.assertTrue(True, list(set([x in p['permissions'] for x in shared]))[0]) \ No newline at end of file diff --git a/test/test_google/test_google_slides.py b/test/test_google/test_google_slides.py new file mode 100644 index 0000000000..f589bffc1d --- /dev/null +++ b/test/test_google/test_google_slides.py @@ -0,0 +1,82 @@ +import unittest +import os +import json +import logging + +from parsons.etl.table import Table +from parsons.google.utitities import setup_google_application_credentials +from parsons.tools.credential_tools import decode_credential +from google.oauth2 import service_account +from googleapiclient.discovery import build +from google.oauth2.service_account import Credentials + +# Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc + +@unittest.skipIf( + not os.environ.get("LIVE_TEST"), "Skipping because not running live test" +) + +class TestGoogleSlides(unittest.TestCase): + def setUp(self): + + self.gs = GoogleSlides() + + # we're going to grab our Test Slides and drop all slides beyond #1 & 2 + presentation = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + self.presentation_id = presentation["presentationId"] + for i in range(len(presentation['slides'])): + if (i+1)>2: + self.gs.delete_slide( + "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", + i+1 + ) + + def test_get_presentation(self): + p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + self.assertEqual(9144000, p['pageSize']['width']['magnitude']) + + def test_get_slide(self): + s = self.gs.get_slide("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", 2) + self.assertEqual("g26d1b1fa556_2_0", s['objectId']) + + def test_duplicate_slide(self): + # duplicating slide #2 to create 3 slides + self.gs.duplicate_slide("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", 2) + p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + self.assertEqual(3, len(p['slides'])) + + def test_replace_slide_text(self): + presentation_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + slide_number=3 + original_text="Replace Text" + replace_text="Parsons is Fun" + self.gs.replace_slide_text( + presentation_id, + slide_number, + original_text, + replace_text + ) + + s = self.gs.get_slide(presentation_id, slide_number) + content = s['pageElements'][0]['shape']['text']['textElements'][1]['textRun']['content'] + self.assertTrue(True, "Parsons is Fun" in content) + + def test_get_slide_images(self): + presentation_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + slide_number=2 + img = self.gs.get_slide_images(presentation_id, slide_number) + height = img[0]['size']['height']['magnitude'] + self.assertEqual(7825, height) + + def test_replace_slide_image(self): + presentation_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + img = self.gs.get_slide_images(presentation_id, 2) + image_obj = img[0] + new_image_url = "https://media.tenor.com/yxJYCVXImqYAAAAM/westwing-josh.gif" + + self.gs.replace_slide_image(presentation_id, 3, image_obj, new_image_url) + img = self.gs.get_slide_images(presentation_id, 3) + self.assertTrue(True, img[0]['image']['sourceUrl']==new_image_url) + + + From 587b90fed87b9f6a713934dca0e200d7472a44e5 Mon Sep 17 00:00:00 2001 From: Elyse Date: Wed, 3 Apr 2024 15:21:54 -0500 Subject: [PATCH 04/11] updates --- .flake8 | 1 - .gitignore | 3 +- parsons/google/google_drive.py | 85 +++++++++++++++----------- parsons/google/google_slides.py | 47 +++++++------- test/test_google/test_google_drive.py | 35 +++++------ test/test_google/test_google_slides.py | 67 +++++++++----------- 6 files changed, 118 insertions(+), 120 deletions(-) diff --git a/.flake8 b/.flake8 index 9d586868b2..7da1f9608e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,2 @@ [flake8] max-line-length = 100 - diff --git a/.gitignore b/.gitignore index a55f944878..a48fb8f277 100644 --- a/.gitignore +++ b/.gitignore @@ -125,5 +125,4 @@ bill_com_credentials.* docs/html docs/dirhtml -*.sw* -settings.json +*.sw* \ No newline at end of file diff --git a/parsons/google/google_drive.py b/parsons/google/google_drive.py index b9b2cb147b..abf319ade9 100644 --- a/parsons/google/google_drive.py +++ b/parsons/google/google_drive.py @@ -25,7 +25,7 @@ class GoogleDrive: def __init__(self, google_keyfile_dict=None, subject=None): - scope = ['https://www.googleapis.com/auth/drive'] + scope = ["https://www.googleapis.com/auth/drive"] setup_google_application_credentials( google_keyfile_dict, "GOOGLE_DRIVE_CREDENTIALS" @@ -37,7 +37,7 @@ def __init__(self, google_keyfile_dict=None, subject=None): credentials_dict, scopes=scope, subject=subject ) - self.client = build('drive', 'v3', credentials=credentials) + self.client = build("drive", "v3", credentials=credentials) def get_permissions(self, file_id): """ @@ -47,19 +47,23 @@ def get_permissions(self, file_id): `Returns:` permission dict """ - + p = self.client.permissions().list(fileId=file_id).execute() return p def _share_object(self, file_id, permission_dict): - + # Send the request to share the file - p = self.client.permissions().create(fileId=file_id, body=permission_dict).execute() + p = ( + self.client.permissions() + .create(fileId=file_id, body=permission_dict) + .execute() + ) return p - def share_object(self, file_id, email_addresses=None, role='reader', type='user'): + def share_object(self, file_id, email_addresses=None, role="reader", type="user"): """ `Args:` file_id: str @@ -71,31 +75,40 @@ def share_object(self, file_id, email_addresses=None, role='reader', type='user' role: str Options are -- owner, organizer, fileOrganizer, writer, commenter, reader https://developers.google.com/drive/api/guides/ref-roles - type: str + type: str Options are -- user, group, domain, anyone `Returns:` List of permission objects """ - if role not in ['owner', 'organizer', 'fileOrganizer', 'writer', 'commenter', 'reader']: - raise Exception(f"{role} not from the allowed list of: \ - owner, organizer, fileOrganizer, writer, commenter, reader") - - if type not in ['user', 'group', 'domain', 'anyone']: - raise Exception(f"{type} not from the allowed list of: \ - user, group, domain, anyone") - - if type=='domain': - permissions = [{ - 'type': type, - 'role': role, - 'domain': email - } for email in email_addresses] + if role not in [ + "owner", + "organizer", + "fileOrganizer", + "writer", + "commenter", + "reader", + ]: + raise Exception( + f"{role} not from the allowed list of: \ + owner, organizer, fileOrganizer, writer, commenter, reader" + ) + + if type not in ["user", "group", "domain", "anyone"]: + raise Exception( + f"{type} not from the allowed list of: \ + user, group, domain, anyone" + ) + + if type == "domain": + permissions = [ + {"type": type, "role": role, "domain": email} + for email in email_addresses + ] else: - permissions = [{ - 'type': type, - 'role': role, - 'emailAddress': email - } for email in email_addresses] + permissions = [ + {"type": type, "role": role, "emailAddress": email} + for email in email_addresses + ] new_permissions = [] for permission in permissions: @@ -117,22 +130,24 @@ def transfer_ownership(self, file_id, new_owner_email): permissions = self.client.permissions().list(fileId=file_id).execute() # Find the current owner - current_owner_permission = next((p for p in permissions.get('permissions', []) if 'owner' in p), None) + current_owner_permission = next( + (p for p in permissions.get("permissions", []) if "owner" in p), None + ) if current_owner_permission: # Update the permission to transfer ownership new_owner_permission = { - 'type': 'user', - 'role': 'owner', - 'emailAddress': new_owner_email + "type": "user", + "role": "owner", + "emailAddress": new_owner_email, } - self.client.permissions().update(fileId=file_id, - permissionId=current_owner_permission['id'], - body=new_owner_permission).execute() + self.client.permissions().update( + fileId=file_id, + permissionId=current_owner_permission["id"], + body=new_owner_permission, + ).execute() logger.info(f"Ownership transferred successfully to {new_owner_email}.") else: logger.info("File does not have a current owner.") return None - - \ No newline at end of file diff --git a/parsons/google/google_slides.py b/parsons/google/google_slides.py index 98d67de320..04b1c8bb9f 100644 --- a/parsons/google/google_slides.py +++ b/parsons/google/google_slides.py @@ -40,7 +40,7 @@ def __init__(self, google_keyfile_dict=None, subject=None): credentials_dict, scopes=scope, subject=subject ) - self.client = build('slides', 'v1', credentials=credentials) + self.client = build("slides", "v1", credentials=credentials) def create_presentation(self, title): """ @@ -84,9 +84,11 @@ def duplicate_slide(self, presentation_id, source_slide_number): the duplicated slide object """ source_slide = self.get_slide(presentation_id, source_slide_number) - source_slide_id = source_slide['objectId'] + source_slide_id = source_slide["objectId"] - batch_request = {"requests": [{"duplicateObject": {"objectId": source_slide_id}}]} + batch_request = { + "requests": [{"duplicateObject": {"objectId": source_slide_id}}] + } response = ( self.client.presentations() .batchUpdate(presentationId=presentation_id, body=batch_request) @@ -125,8 +127,7 @@ def delete_slide(self, presentation_id, slide_number): """ slide = self.get_slide(presentation_id, slide_number) self.client.presentations().pages().delete( - presentationId=presentation_id, - pageObjectId=slide['objectId'] + presentationId=presentation_id, pageObjectId=slide["objectId"] ).execute() return None @@ -149,7 +150,7 @@ def replace_slide_text( """ slide = self.get_slide(presentation_id, slide_number) - slide_id = slide['objectId'] + slide_id = slide["objectId"] reqs = [ { @@ -181,13 +182,15 @@ def get_slide_images(self, presentation_id, slide_number): slide = self.get_slide(presentation_id, slide_number) images = [] - for x in slide['pageElements']: - if 'image' in x.keys(): + for x in slide["pageElements"]: + if "image" in x.keys(): images.append(x) return images - def replace_slide_image(self, presentation_id, slide_number, image_obj, new_image_url): + def replace_slide_image( + self, presentation_id, slide_number, image_obj, new_image_url + ): """ `Args:` presentation_id: str @@ -205,21 +208,21 @@ def replace_slide_image(self, presentation_id, slide_number, image_obj, new_imag slide = self.get_slide(presentation_id, slide_number) reqs = [ - { - "createImage": { - "url": new_image_url, - "elementProperties": { - "pageObjectId": slide["objectId"], - "size": image_obj["size"], - "transform": image_obj["transform"], - }, - } - }, - {"deleteObject": {"objectId": image_obj["objectId"]}}, - ] + { + "createImage": { + "url": new_image_url, + "elementProperties": { + "pageObjectId": slide["objectId"], + "size": image_obj["size"], + "transform": image_obj["transform"], + }, + } + }, + {"deleteObject": {"objectId": image_obj["objectId"]}}, + ] self.client.presentations().batchUpdate( body={"requests": reqs}, presentationId=presentation_id ).execute() - return None \ No newline at end of file + return None diff --git a/test/test_google/test_google_drive.py b/test/test_google/test_google_drive.py index 5cd94995c8..14b2c573e8 100644 --- a/test/test_google/test_google_drive.py +++ b/test/test_google/test_google_drive.py @@ -1,35 +1,28 @@ import unittest import os -import json -import logging - -from parsons.etl.table import Table -from parsons.google.utitities import setup_google_application_credentials -from parsons.tools.credential_tools import decode_credential -from google.oauth2 import service_account -from googleapiclient.discovery import build -from google.oauth2.service_account import Credentials +from parsons import GoogleDrive # Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc + @unittest.skipIf( not os.environ.get("LIVE_TEST"), "Skipping because not running live test" ) - -def setUp(self): +class TestGoogleDrive(unittest.TestCase): + def setUp(self): self.gd = GoogleDrive() -def test_get_permissions(self): + def test_get_permissions(self): + + file_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + p = self.gd.get_permissinos(file_id) + self.assertTrue(True, "anyoneWithLink" in [x["id"] for x in p["permissions"]]) - file_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - p = self.gd.get_permissinos(file_id) - self.assertTrue(True, 'anyoneWithLink' in [x['id'] for x in p['permissions']]) + def test_share_object(self): -def test_share_object(self): - - file_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - email_addresses=["bob@bob.com"] + file_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + email_addresses = ["bob@bob.com"] - shared = self.gd.share_object(file_id, email_addresses) - self.assertTrue(True, list(set([x in p['permissions'] for x in shared]))[0]) \ No newline at end of file + shared = self.gd.share_object(file_id, email_addresses) + self.assertTrue(True, list(set([x in p["permissions"] for x in shared]))[0]) diff --git a/test/test_google/test_google_slides.py b/test/test_google/test_google_slides.py index f589bffc1d..4ef19ea752 100644 --- a/test/test_google/test_google_slides.py +++ b/test/test_google/test_google_slides.py @@ -1,82 +1,71 @@ import unittest import os -import json -import logging - -from parsons.etl.table import Table -from parsons.google.utitities import setup_google_application_credentials -from parsons.tools.credential_tools import decode_credential -from google.oauth2 import service_account -from googleapiclient.discovery import build -from google.oauth2.service_account import Credentials +from parsons import GoogleSlides # Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc + @unittest.skipIf( not os.environ.get("LIVE_TEST"), "Skipping because not running live test" ) - class TestGoogleSlides(unittest.TestCase): def setUp(self): self.gs = GoogleSlides() # we're going to grab our Test Slides and drop all slides beyond #1 & 2 - presentation = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + presentation = self.gs.get_presentation( + "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + ) self.presentation_id = presentation["presentationId"] - for i in range(len(presentation['slides'])): - if (i+1)>2: + for i in range(len(presentation["slides"])): + if (i + 1) > 2: self.gs.delete_slide( - "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", - i+1 + "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", i + 1 ) def test_get_presentation(self): - p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") - self.assertEqual(9144000, p['pageSize']['width']['magnitude']) - + p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + self.assertEqual(9144000, p["pageSize"]["width"]["magnitude"]) + def test_get_slide(self): s = self.gs.get_slide("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", 2) - self.assertEqual("g26d1b1fa556_2_0", s['objectId']) + self.assertEqual("g26d1b1fa556_2_0", s["objectId"]) def test_duplicate_slide(self): # duplicating slide #2 to create 3 slides self.gs.duplicate_slide("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", 2) - p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") - self.assertEqual(3, len(p['slides'])) + p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + self.assertEqual(3, len(p["slides"])) def test_replace_slide_text(self): - presentation_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - slide_number=3 - original_text="Replace Text" - replace_text="Parsons is Fun" + presentation_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + slide_number = 3 + original_text = "Replace Text" + replace_text = "Parsons is Fun" self.gs.replace_slide_text( - presentation_id, - slide_number, - original_text, - replace_text + presentation_id, slide_number, original_text, replace_text ) s = self.gs.get_slide(presentation_id, slide_number) - content = s['pageElements'][0]['shape']['text']['textElements'][1]['textRun']['content'] + content = s["pageElements"][0]["shape"]["text"]["textElements"][1]["textRun"][ + "content" + ] self.assertTrue(True, "Parsons is Fun" in content) def test_get_slide_images(self): - presentation_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - slide_number=2 + presentation_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + slide_number = 2 img = self.gs.get_slide_images(presentation_id, slide_number) - height = img[0]['size']['height']['magnitude'] + height = img[0]["size"]["height"]["magnitude"] self.assertEqual(7825, height) - + def test_replace_slide_image(self): - presentation_id="19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + presentation_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" img = self.gs.get_slide_images(presentation_id, 2) image_obj = img[0] new_image_url = "https://media.tenor.com/yxJYCVXImqYAAAAM/westwing-josh.gif" self.gs.replace_slide_image(presentation_id, 3, image_obj, new_image_url) img = self.gs.get_slide_images(presentation_id, 3) - self.assertTrue(True, img[0]['image']['sourceUrl']==new_image_url) - - - + self.assertTrue(True, img[0]["image"]["sourceUrl"] == new_image_url) From 1fa27f0c7ae9681786781aef7f42abdfe14968bf Mon Sep 17 00:00:00 2001 From: Elyse Date: Wed, 3 Apr 2024 15:24:48 -0500 Subject: [PATCH 05/11] remove bin --- .../github/parsons-fork/bin/Activate.ps1 | 241 ------------------ .../github/parsons-fork/bin/activate | 76 ------ .../github/parsons-fork/bin/activate.csh | 37 --- .../github/parsons-fork/bin/activate.fish | 75 ------ .../github/parsons-fork/bin/easy_install | 8 - .../github/parsons-fork/bin/easy_install-3.8 | 8 - .../elyseweiss/github/parsons-fork/bin/pip | 8 - .../elyseweiss/github/parsons-fork/bin/pip3 | 8 - .../elyseweiss/github/parsons-fork/bin/pip3.8 | 8 - .../elyseweiss/github/parsons-fork/bin/python | 1 - .../github/parsons-fork/bin/python3 | 1 - .../elyseweiss/github/parsons-fork/pyvenv.cfg | 3 - 12 files changed, 474 deletions(-) delete mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 delete mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/activate delete mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/activate.csh delete mode 100644 $/Users/elyseweiss/github/parsons-fork/bin/activate.fish delete mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/easy_install delete mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 delete mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/pip delete mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/pip3 delete mode 100755 $/Users/elyseweiss/github/parsons-fork/bin/pip3.8 delete mode 120000 $/Users/elyseweiss/github/parsons-fork/bin/python delete mode 120000 $/Users/elyseweiss/github/parsons-fork/bin/python3 delete mode 100644 $/Users/elyseweiss/github/parsons-fork/pyvenv.cfg diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 b/$/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 deleted file mode 100644 index 2fb3852c3c..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/Activate.ps1 +++ /dev/null @@ -1,241 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/activate b/$/Users/elyseweiss/github/parsons-fork/bin/activate deleted file mode 100644 index 7e0dc0d3e8..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/activate +++ /dev/null @@ -1,76 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # This should detect bash and zsh, which have a hash command that must - # be called to get it to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r - fi - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV="/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork" -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/bin:$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - if [ "x(parsons-fork) " != x ] ; then - PS1="(parsons-fork) ${PS1:-}" - else - if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then - # special case for Aspen magic directories - # see https://aspen.io/ - PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" - else - PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" - fi - fi - export PS1 -fi - -# This should detect bash and zsh, which have a hash command that must -# be called to get it to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r -fi diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/activate.csh b/$/Users/elyseweiss/github/parsons-fork/bin/activate.csh deleted file mode 100644 index 83dfa1795f..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/activate.csh +++ /dev/null @@ -1,37 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. -# Created by Davide Di Blasi . -# Ported to Python 3.3 venv by Andrew Svetlov - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV "/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork" - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/bin:$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - if ("parsons-fork" != "") then - set env_name = "parsons-fork" - else - if (`basename "VIRTUAL_ENV"` == "__") then - # special case for Aspen magic directories - # see https://aspen.io/ - set env_name = `basename \`dirname "$VIRTUAL_ENV"\`` - else - set env_name = `basename "$VIRTUAL_ENV"` - endif - endif - set prompt = "[$env_name] $prompt" - unset env_name -endif - -alias pydoc python -m pydoc - -rehash diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/activate.fish b/$/Users/elyseweiss/github/parsons-fork/bin/activate.fish deleted file mode 100644 index 8ecf90ace6..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/activate.fish +++ /dev/null @@ -1,75 +0,0 @@ -# This file must be used with ". bin/activate.fish" *from fish* (http://fishshell.org) -# you cannot run it directly - -function deactivate -d "Exit virtualenv and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - functions -e fish_prompt - set -e _OLD_FISH_PROMPT_OVERRIDE - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - - set -e VIRTUAL_ENV - if test "$argv[1]" != "nondestructive" - # Self destruct! - functions -e deactivate - end -end - -# unset irrelevant variables -deactivate nondestructive - -set -gx VIRTUAL_ENV "/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork" - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/bin" $PATH - -# unset PYTHONHOME if set -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # save the current fish_prompt function as the function _old_fish_prompt - functions -c fish_prompt _old_fish_prompt - - # with the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command - set -l old_status $status - - # Prompt override? - if test -n "(parsons-fork) " - printf "%s%s" "(parsons-fork) " (set_color normal) - else - # ...Otherwise, prepend env - set -l _checkbase (basename "$VIRTUAL_ENV") - if test $_checkbase = "__" - # special case for Aspen magic directories - # see https://aspen.io/ - printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal) - else - printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal) - end - end - - # Restore the return status of the previous command. - echo "exit $old_status" | . - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" -end diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/easy_install b/$/Users/elyseweiss/github/parsons-fork/bin/easy_install deleted file mode 100755 index 1e296a0ab5..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/easy_install +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from setuptools.command.easy_install import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 b/$/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 deleted file mode 100755 index 1e296a0ab5..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/easy_install-3.8 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from setuptools.command.easy_install import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/pip b/$/Users/elyseweiss/github/parsons-fork/bin/pip deleted file mode 100755 index ee95acbfe6..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/pip3 b/$/Users/elyseweiss/github/parsons-fork/bin/pip3 deleted file mode 100755 index ee95acbfe6..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/pip3.8 b/$/Users/elyseweiss/github/parsons-fork/bin/pip3.8 deleted file mode 100755 index ee95acbfe6..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/pip3.8 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/elyseweiss/github/parsons-fork/$/Users/elyseweiss/github/parsons-fork/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/python b/$/Users/elyseweiss/github/parsons-fork/bin/python deleted file mode 120000 index 20e95ae78d..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/python +++ /dev/null @@ -1 +0,0 @@ -/Users/elyseweiss/Environments/parsons/bin/python \ No newline at end of file diff --git a/$/Users/elyseweiss/github/parsons-fork/bin/python3 b/$/Users/elyseweiss/github/parsons-fork/bin/python3 deleted file mode 120000 index d8654aa0e2..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -python \ No newline at end of file diff --git a/$/Users/elyseweiss/github/parsons-fork/pyvenv.cfg b/$/Users/elyseweiss/github/parsons-fork/pyvenv.cfg deleted file mode 100644 index 3467eb3ba9..0000000000 --- a/$/Users/elyseweiss/github/parsons-fork/pyvenv.cfg +++ /dev/null @@ -1,3 +0,0 @@ -home = /Users/elyseweiss/Environments/parsons/bin -include-system-site-packages = false -version = 3.8.6 From 30a4f9a7dd9efb8702cac29b5ec1cdbf4bbcc502 Mon Sep 17 00:00:00 2001 From: Elyse Weiss Date: Tue, 7 May 2024 14:23:48 -0500 Subject: [PATCH 06/11] shauna comments --- test/test_google/test_google_drive.py | 2 +- test/test_google/test_google_slides.py | 27 ++++++++++---------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/test/test_google/test_google_drive.py b/test/test_google/test_google_drive.py index 14b2c573e8..afdc2d4172 100644 --- a/test/test_google/test_google_drive.py +++ b/test/test_google/test_google_drive.py @@ -16,7 +16,7 @@ def setUp(self): def test_get_permissions(self): file_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - p = self.gd.get_permissinos(file_id) + p = self.gd.get_permissions(file_id) self.assertTrue(True, "anyoneWithLink" in [x["id"] for x in p["permissions"]]) def test_share_object(self): diff --git a/test/test_google/test_google_slides.py b/test/test_google/test_google_slides.py index 4ef19ea752..05394a3b54 100644 --- a/test/test_google/test_google_slides.py +++ b/test/test_google/test_google_slides.py @@ -3,43 +3,36 @@ from parsons import GoogleSlides # Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc +slides_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - -@unittest.skipIf( - not os.environ.get("LIVE_TEST"), "Skipping because not running live test" -) class TestGoogleSlides(unittest.TestCase): def setUp(self): self.gs = GoogleSlides() # we're going to grab our Test Slides and drop all slides beyond #1 & 2 - presentation = self.gs.get_presentation( - "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - ) + presentation = self.gs.get_presentation(slides_id) self.presentation_id = presentation["presentationId"] for i in range(len(presentation["slides"])): if (i + 1) > 2: - self.gs.delete_slide( - "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", i + 1 - ) + self.gs.delete_slide(slides_id, i + 1) def test_get_presentation(self): - p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + p = self.gs.get_presentation(slides_id) self.assertEqual(9144000, p["pageSize"]["width"]["magnitude"]) def test_get_slide(self): - s = self.gs.get_slide("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", 2) + s = self.gs.get_slide(slides_id, 2) self.assertEqual("g26d1b1fa556_2_0", s["objectId"]) def test_duplicate_slide(self): # duplicating slide #2 to create 3 slides - self.gs.duplicate_slide("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc", 2) - p = self.gs.get_presentation("19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc") + self.gs.duplicate_slide(slides_id, 2) + p = self.gs.get_presentation(slides_id) self.assertEqual(3, len(p["slides"])) def test_replace_slide_text(self): - presentation_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + presentation_id = slides_id slide_number = 3 original_text = "Replace Text" replace_text = "Parsons is Fun" @@ -54,14 +47,14 @@ def test_replace_slide_text(self): self.assertTrue(True, "Parsons is Fun" in content) def test_get_slide_images(self): - presentation_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + presentation_id = slides_id slide_number = 2 img = self.gs.get_slide_images(presentation_id, slide_number) height = img[0]["size"]["height"]["magnitude"] self.assertEqual(7825, height) def test_replace_slide_image(self): - presentation_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" + presentation_id = slides_id img = self.gs.get_slide_images(presentation_id, 2) image_obj = img[0] new_image_url = "https://media.tenor.com/yxJYCVXImqYAAAAM/westwing-josh.gif" From a0011332b769e0695d99a18bd5535144cd15eaad Mon Sep 17 00:00:00 2001 From: Elyse Weiss Date: Tue, 7 May 2024 15:09:13 -0500 Subject: [PATCH 07/11] docs --- docs/google.rst | 112 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/docs/google.rst b/docs/google.rst index 9590d51f20..1ef32557cf 100644 --- a/docs/google.rst +++ b/docs/google.rst @@ -51,7 +51,6 @@ API .. autoclass:: parsons.google.google_admin.GoogleAdmin :inherited-members: - ******** BigQuery ******** @@ -258,16 +257,69 @@ API .. autoclass :: parsons.google.google_civic.GoogleCivic :inherited-members: +************* +Google Drive +************* + +======== +Overview +======== + +The GoogleDrive class allows you to interact with Google Drive. You can update permissions with this connector. + +In order to instantiate the class, you must pass Google service account credentials as a dictionary, or store the credentials as a JSON file locally and pass the path to the file as a string in the ``GOOGLE_DRIVE_CREDENTIALS`` environment variable. You can follow these steps: + +- Go to the `Google Developer Console `_ and make sure the "Google Drive API" is enabled. +- Go to the credentials page via the lefthand sidebar. On the credentials page, click "create credentials". +- Choose the "Service Account" option and fill out the form provided. This should generate your credentials. +- Select your newly created Service Account on the credentials main page. +- select "keys", then "add key", then "create new key". Pick the key type JSON. The credentials should start to automatically download. + +You can now copy and paste the data from the key into your script or (recommended) save it locally as a JSON file. + +========== +Quickstart +========== + +To instantiate the GoogleSheets class, you can either pass the constructor a dict containing your Google service account credentials or define the environment variable ``GOOGLE_DRIVE_CREDENTIALS`` to contain a path to the JSON file containing the dict. + +.. code-block:: python + + from parsons import GoogleDrive + + # First approach: Use API credentials via environmental variables + drive = GoogleDrive() + + # Second approach: Pass API credentials as argument + credential_filename = 'google_drive_service_credentials.json' + credentials = json.load(open(credential_filename)) + drive = GoogleDrive(google_keyfile_dict=credentials) + +You can then retreive and edit the permissions for Google Drive objects. + +.. code-block:: python + + email_addresses = ["bob@bob.com"] + shared = drive.share_object(file_id, email_addresses) + + +=== +API +=== + +.. autoclass:: parsons.google.google_drive.GoogleDrive + :inherited-members: + ************* -Google Sheets +Google Slides ************* ======== Overview ======== -The GoogleSheets class allows you to interact with Google service account spreadsheets, called "Google Sheets." You can create, modify, read, format, share and delete sheets with this connector. +The GoogleSlides class allows you to interact with Google Slides. You can create and modify Google Slides with this connector. In order to instantiate the class, you must pass Google service account credentials as a dictionary, or store the credentials as a JSON file locally and pass the path to the file as a string in the ``GOOGLE_DRIVE_CREDENTIALS`` environment variable. You can follow these steps: @@ -314,3 +366,57 @@ API .. autoclass:: parsons.google.google_sheets.GoogleSheets :inherited-members: +************* +Google Sheets +************* + +======== +Overview +======== + +The GoogleSheets class allows you to interact with Google service account spreadsheets, called "Google Sheets." You can create, modify, read, format, share and delete sheets with this connector. + +In order to instantiate the class, you must pass Google service account credentials as a dictionary, or store the credentials as a JSON file locally and pass the path to the file as a string in the ``GOOGLE_DRIVE_CREDENTIALS`` environment variable. You can follow these steps: + +- Go to the `Google Developer Console `_ and make sure the "Google Drive API" and the "Google Sheets API" are both enabled. +- Go to the credentials page via the lefthand sidebar. On the credentials page, click "create credentials". +- Choose the "Service Account" option and fill out the form provided. This should generate your credentials. +- Select your newly created Service Account on the credentials main page. +- select "keys", then "add key", then "create new key". Pick the key type JSON. The credentials should start to automatically download. + +You can now copy and paste the data from the key into your script or (recommended) save it locally as a JSON file. + +========== +Quickstart +========== + +To instantiate the GoogleSheets class, you can either pass the constructor a dict containing your Google service account credentials or define the environment variable ``GOOGLE_DRIVE_CREDENTIALS`` to contain a path to the JSON file containing the dict. + +.. code-block:: python + + from parsons import GoogleSlides + + # First approach: Use API credentials via environmental variables + slides = GoogleSlides() + + # Second approach: Pass API credentials as argument + credential_filename = 'google_drive_service_credentials.json' + credentials = json.load(open(credential_filename)) + slides = GoogleSlides(google_keyfile_dict=credentials) + +You can then create/modify/retrieve slides using instance methods: + +.. code-block:: python + + slides.create_presentation("Parsons is Fun") + + +You can use the GoogleDrive() connector to share the slide deck. + +=== +API +=== + +.. autoclass:: parsons.google.google_slides.GoogleSlides + :inherited-members: + From 3c81c14659fd9a491f12aae5e085cf43ab8510a1 Mon Sep 17 00:00:00 2001 From: Elyse Weiss Date: Tue, 7 May 2024 15:12:22 -0500 Subject: [PATCH 08/11] title --- docs/google.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/google.rst b/docs/google.rst index 1ef32557cf..9319bba27b 100644 --- a/docs/google.rst +++ b/docs/google.rst @@ -312,7 +312,7 @@ API ************* -Google Slides +Google Sheets ************* ======== @@ -367,7 +367,7 @@ API :inherited-members: ************* -Google Sheets +Google Slides ************* ======== From 4dd1053ea9ba2cb6596e96f643edbc0c46e9ea68 Mon Sep 17 00:00:00 2001 From: Elyse Weiss Date: Tue, 7 May 2024 15:13:15 -0500 Subject: [PATCH 09/11] api --- docs/google.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/google.rst b/docs/google.rst index 9319bba27b..72363bc131 100644 --- a/docs/google.rst +++ b/docs/google.rst @@ -254,7 +254,7 @@ You can also retrieve represntative information such as offices, officals, etc. API === -.. autoclass :: parsons.google.google_civic.GoogleCivic +.. autoclass:: parsons.google.google_civic.GoogleCivic :inherited-members: ************* From 61014fce5a2f563872313c91eceef594f5edc46c Mon Sep 17 00:00:00 2001 From: Elyse Weiss Date: Thu, 23 May 2024 13:52:03 -0500 Subject: [PATCH 10/11] goodbye flake8 --- .flake8 | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 7da1f9608e..0000000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 100 From 68c36855e4ae7e9891ef66ea1d91a43b07ca6516 Mon Sep 17 00:00:00 2001 From: Elyse Weiss Date: Wed, 29 May 2024 16:45:58 -0500 Subject: [PATCH 11/11] getting tests to work --- parsons/google/google_slides.py | 20 ++++++++++++++--- test/test_google/test_google_drive.py | 11 +++++++--- test/test_google/test_google_slides.py | 30 +++++++++++++------------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/parsons/google/google_slides.py b/parsons/google/google_slides.py index 04b1c8bb9f..92ff6584ce 100644 --- a/parsons/google/google_slides.py +++ b/parsons/google/google_slides.py @@ -125,9 +125,23 @@ def delete_slide(self, presentation_id, slide_number): `Returns:` None """ - slide = self.get_slide(presentation_id, slide_number) - self.client.presentations().pages().delete( - presentationId=presentation_id, pageObjectId=slide["objectId"] + slide_object_id = self.get_slide(presentation_id, slide_number)['objectId'] + + requests = [ + { + 'deleteObject': { + 'objectId': slide_object_id + } + } + ] + + # Execute the request + body = { + 'requests': requests + } + self.client.presentations().batchUpdate( + presentationId=presentation_id, + body=body ).execute() return None diff --git a/test/test_google/test_google_drive.py b/test/test_google/test_google_drive.py index afdc2d4172..f42ff670b5 100644 --- a/test/test_google/test_google_drive.py +++ b/test/test_google/test_google_drive.py @@ -1,6 +1,8 @@ import unittest import os from parsons import GoogleDrive +import random +import string # Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc @@ -22,7 +24,10 @@ def test_get_permissions(self): def test_share_object(self): file_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" - email_addresses = ["bob@bob.com"] + email = ''.join(random.choices(string.ascii_letters, k=10))+"@gmail.com" + email_addresses = [email] - shared = self.gd.share_object(file_id, email_addresses) - self.assertTrue(True, list(set([x in p["permissions"] for x in shared]))[0]) + before = self.gd.get_permissions(file_id)['permissions'] + self.gd.share_object(file_id, email_addresses) + after = self.gd.get_permissions(file_id)['permissions'] + self.assertTrue(True, len(after) > len(before)) diff --git a/test/test_google/test_google_slides.py b/test/test_google/test_google_slides.py index 05394a3b54..f5fbbf7655 100644 --- a/test/test_google/test_google_slides.py +++ b/test/test_google/test_google_slides.py @@ -5,6 +5,9 @@ # Test Slides: https://docs.google.com/presentation/d/19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc slides_id = "19I-kicyaJV53KoPNwt77KJL10fHzWFdZ_c2mW4XJaxc" +@unittest.skipIf( + not os.environ.get("LIVE_TEST"), "Skipping because not running live test" +) class TestGoogleSlides(unittest.TestCase): def setUp(self): @@ -12,9 +15,9 @@ def setUp(self): # we're going to grab our Test Slides and drop all slides beyond #1 & 2 presentation = self.gs.get_presentation(slides_id) - self.presentation_id = presentation["presentationId"] - for i in range(len(presentation["slides"])): - if (i + 1) > 2: + slides = presentation["slides"] + if len(slides)>2: + for i in range(2, len(slides)): self.gs.delete_slide(slides_id, i + 1) def test_get_presentation(self): @@ -22,8 +25,8 @@ def test_get_presentation(self): self.assertEqual(9144000, p["pageSize"]["width"]["magnitude"]) def test_get_slide(self): - s = self.gs.get_slide(slides_id, 2) - self.assertEqual("g26d1b1fa556_2_0", s["objectId"]) + s = self.gs.get_slide(slides_id, 1) + self.assertEqual("p", s["objectId"]) def test_duplicate_slide(self): # duplicating slide #2 to create 3 slides @@ -32,33 +35,30 @@ def test_duplicate_slide(self): self.assertEqual(3, len(p["slides"])) def test_replace_slide_text(self): - presentation_id = slides_id - slide_number = 3 + self.gs.duplicate_slide(slides_id, 2) original_text = "Replace Text" replace_text = "Parsons is Fun" self.gs.replace_slide_text( - presentation_id, slide_number, original_text, replace_text + slides_id, 3, original_text, replace_text ) - s = self.gs.get_slide(presentation_id, slide_number) + s = self.gs.get_slide(slides_id, 3) content = s["pageElements"][0]["shape"]["text"]["textElements"][1]["textRun"][ "content" ] self.assertTrue(True, "Parsons is Fun" in content) def test_get_slide_images(self): - presentation_id = slides_id - slide_number = 2 - img = self.gs.get_slide_images(presentation_id, slide_number) + img = self.gs.get_slide_images(slides_id, 2) height = img[0]["size"]["height"]["magnitude"] - self.assertEqual(7825, height) + self.assertEqual(22525, height) def test_replace_slide_image(self): presentation_id = slides_id - img = self.gs.get_slide_images(presentation_id, 2) + self.gs.duplicate_slide(slides_id, 2) + img = self.gs.get_slide_images(presentation_id, 3) image_obj = img[0] new_image_url = "https://media.tenor.com/yxJYCVXImqYAAAAM/westwing-josh.gif" - self.gs.replace_slide_image(presentation_id, 3, image_obj, new_image_url) img = self.gs.get_slide_images(presentation_id, 3) self.assertTrue(True, img[0]["image"]["sourceUrl"] == new_image_url)