From 655beba5569bb6927a4b2ede64a841a9482a4551 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andy=20M=C3=A9ry?= <amery@scaleway.com>
Date: Thu, 15 Dec 2022 17:48:56 +0100
Subject: [PATCH] feat: Support Scaleway provider

---
 README.md                                 | 25 +++++++++++++++++
 index.js                                  |  5 +++-
 lib/inject.js                             | 23 +++++++++++++--
 test.js                                   | 17 ++++++++++++
 tests/scaleway_provider/_slimPatterns.yml |  2 ++
 tests/scaleway_provider/handler.py        |  5 ++++
 tests/scaleway_provider/package.json      | 15 ++++++++++
 tests/scaleway_provider/requirements.txt  |  3 ++
 tests/scaleway_provider/serverless.yml    | 34 +++++++++++++++++++++++
 9 files changed, 126 insertions(+), 3 deletions(-)
 create mode 100644 tests/scaleway_provider/_slimPatterns.yml
 create mode 100644 tests/scaleway_provider/handler.py
 create mode 100644 tests/scaleway_provider/package.json
 create mode 100644 tests/scaleway_provider/requirements.txt
 create mode 100644 tests/scaleway_provider/serverless.yml

diff --git a/README.md b/README.md
index 91172bf9..d9127adb 100644
--- a/README.md
+++ b/README.md
@@ -580,6 +580,31 @@ package:
     - '**'
 ```
 
+## Custom Provider Support
+
+### Scaleway
+
+This plugin is compatible with the [Scaleway Serverless Framework Plugin](https://github.com/scaleway/serverless-scaleway-functions) to package dependencies for Python functions deployed on [Scaleway](https://www.scaleway.com/en/serverless-functions/). To use it, add the following to your `serverless.yml`:
+
+```yaml
+provider:
+  name: scaleway
+  runtime: python311
+
+plugins:
+  - serverless-python-requirements
+  - serverless-scaleway-functions
+```
+
+To handle native dependencies, it's recommended to use the Docker builder with the image provided by Scaleway:
+
+```yaml
+custom:
+  pythonRequirements:
+    # Can use any Python version supported by Scaleway
+    dockerImage: rg.fr-par.scw.cloud/scwfunctionsruntimes-public/python-dep:3.11
+```
+
 ## Contributors
 
 - [@dschep](https://github.com/dschep) - Original developer
diff --git a/index.js b/index.js
index 246b121e..25cc34cd 100644
--- a/index.js
+++ b/index.js
@@ -72,7 +72,10 @@ class ServerlessPythonRequirements {
     ) {
       options.pythonBin = 'python';
     }
-
+    if (/python3[0-9]+/.test(options.pythonBin)) {
+      // "google" and "scaleway" providers' runtimes uses python3XX
+      options.pythonBin = options.pythonBin.replace(/3([0-9]+)/, '3.$1');
+    }
     if (options.dockerizePip === 'non-linux') {
       options.dockerizePip = process.platform !== 'linux';
     }
diff --git a/lib/inject.js b/lib/inject.js
index ea20e58d..12267376 100644
--- a/lib/inject.js
+++ b/lib/inject.js
@@ -13,10 +13,16 @@ BbPromise.promisifyAll(fse);
  * Inject requirements into packaged application.
  * @param {string} requirementsPath requirements folder path
  * @param {string} packagePath target package path
+ * @param {string} injectionRelativePath installation directory in target package
  * @param {Object} options our options object
  * @return {Promise} the JSZip object constructed.
  */
-function injectRequirements(requirementsPath, packagePath, options) {
+function injectRequirements(
+  requirementsPath,
+  packagePath,
+  injectionRelativePath,
+  options
+) {
   const noDeploy = new Set(options.noDeploy || []);
 
   return fse
@@ -29,7 +35,13 @@ function injectRequirements(requirementsPath, packagePath, options) {
           dot: true,
         })
       )
-        .map((file) => [file, path.relative(requirementsPath, file)])
+        .map((file) => [
+          file,
+          path.join(
+            injectionRelativePath,
+            path.relative(requirementsPath, file)
+          ),
+        ])
         .filter(
           ([file, relativeFile]) =>
             !file.endsWith('/') &&
@@ -101,6 +113,11 @@ async function injectAllRequirements(funcArtifact) {
     this.serverless.cli.log('Injecting required Python packages to package...');
   }
 
+  let injectionRelativePath = '.';
+  if (this.serverless.service.provider.name == 'scaleway') {
+    injectionRelativePath = 'package';
+  }
+
   try {
     if (this.serverless.service.package.individually) {
       await BbPromise.resolve(this.targetFuncs)
@@ -138,6 +155,7 @@ async function injectAllRequirements(funcArtifact) {
             : injectRequirements(
                 path.join('.serverless', func.module, 'requirements'),
                 func.package.artifact,
+                injectionRelativePath,
                 this.options
               );
         });
@@ -145,6 +163,7 @@ async function injectAllRequirements(funcArtifact) {
       await injectRequirements(
         path.join('.serverless', 'requirements'),
         this.serverless.service.package.artifact || funcArtifact,
+        injectionRelativePath,
         this.options
       );
     }
diff --git a/test.js b/test.js
index f4afca45..fad21273 100644
--- a/test.js
+++ b/test.js
@@ -1729,3 +1729,20 @@ test('poetry py3.9 only installs optional packages specified in onlyGroups', asy
   t.true(zipfiles.includes(`boto3${sep}__init__.py`), 'boto3 is packaged');
   t.end();
 });
+
+test('py3.7 injects dependencies into `package` folder when using scaleway provider', async (t) => {
+  process.chdir('tests/scaleway_provider');
+  const path = npm(['pack', '../..']);
+  npm(['i', path]);
+  sls(['package'], { env: {} });
+  const zipfiles = await listZipFiles('.serverless/sls-py-req-test.zip');
+  t.true(
+    zipfiles.includes(`package${sep}flask${sep}__init__.py`),
+    'flask is packaged'
+  );
+  t.true(
+    zipfiles.includes(`package${sep}boto3${sep}__init__.py`),
+    'boto3 is packaged'
+  );
+  t.end();
+});
diff --git a/tests/scaleway_provider/_slimPatterns.yml b/tests/scaleway_provider/_slimPatterns.yml
new file mode 100644
index 00000000..443af9a0
--- /dev/null
+++ b/tests/scaleway_provider/_slimPatterns.yml
@@ -0,0 +1,2 @@
+slimPatterns:
+  - '**/__main__.py'
diff --git a/tests/scaleway_provider/handler.py b/tests/scaleway_provider/handler.py
new file mode 100644
index 00000000..5e2e67ff
--- /dev/null
+++ b/tests/scaleway_provider/handler.py
@@ -0,0 +1,5 @@
+import requests
+
+
+def hello(event, context):
+    return requests.get('https://httpbin.org/get').json()
diff --git a/tests/scaleway_provider/package.json b/tests/scaleway_provider/package.json
new file mode 100644
index 00000000..d54b88e0
--- /dev/null
+++ b/tests/scaleway_provider/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "example",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "serverless-python-requirements": "file:serverless-python-requirements-6.0.0.tgz",
+    "serverless-scaleway-functions": "^0.4.8"
+  }
+}
diff --git a/tests/scaleway_provider/requirements.txt b/tests/scaleway_provider/requirements.txt
new file mode 100644
index 00000000..23bfb7a6
--- /dev/null
+++ b/tests/scaleway_provider/requirements.txt
@@ -0,0 +1,3 @@
+flask==0.12.5
+bottle
+boto3
diff --git a/tests/scaleway_provider/serverless.yml b/tests/scaleway_provider/serverless.yml
new file mode 100644
index 00000000..5d827bdf
--- /dev/null
+++ b/tests/scaleway_provider/serverless.yml
@@ -0,0 +1,34 @@
+service: sls-py-req-test
+
+configValidationMode: off
+
+provider:
+  name: scaleway
+  runtime: python39
+
+plugins:
+  - serverless-python-requirements
+  - serverless-scaleway-functions
+
+custom:
+  pythonRequirements:
+    zip: ${env:zip, self:custom.defaults.zip}
+    slim: ${env:slim, self:custom.defaults.slim}
+    slimPatterns: ${file(./slimPatterns.yml):slimPatterns, self:custom.defaults.slimPatterns}
+    slimPatternsAppendDefaults: ${env:slimPatternsAppendDefaults, self:custom.defaults.slimPatternsAppendDefaults}
+    dockerizePip: ${env:dockerizePip, self:custom.defaults.dockerizePip}
+  defaults:
+    zip: false
+    slimPatterns: false
+    slimPatternsAppendDefaults: true
+    slim: false
+    dockerizePip: false
+
+package:
+  patterns:
+    - '!**/*'
+    - 'handler.py'
+
+functions:
+  hello:
+    handler: handler.hello