Repository: Shopify/shopify_python_api

Issue:
`Scope` param required in create_permission_url func but should be omitted for managed installations
# Issue summary

`session.create_permission_url()` currently requires the `scope` param to be provided, but Shopify API docs specify that this should be omitted for managed installations where the scopes are defined in the app's TOML file.

## Expected behavior

Function should create url with query params, with or without provided scopes. 


## Actual behavior

Function returns missing parameter `scope` upon execution if not provided.

## Steps to reproduce the problem

1. Create Public or Private Custom App with scopes defined in app's TOML
2. Install app on development store using Shopify managed installation
3. Call session.create_permission_url() with only `redirect_uri` and `state`

## Reduced test case
```
import shopify
import binascii

SHOP_URL = 'myshop'
API_VERSION = '2025-01'

def authenticate(request):
  redirect_url = 'https://myapp.com/shopify/auth/finalize'
  state = binascii.b2a_hex(os.urandom(15)).decode("utf-8")
  
  _new_session = shopify.Session(SHOP_URL, API_VERSION)
  auth_url = _new_session.create_permission_url(redirect_uri, state)
  
  return redirect(auth_url)
```
---

## Checklist

- [x] I have described this issue in a way that is actionable (if possible)

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG b/CHANGELOG
index 50cae06e..e9910c2e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,7 @@
 == Unreleased
 
+- Remove requirement to provide scopes to Permission URL, as it should be omitted if defined with the TOML file.
+
 == Version 12.7.0
 
 - Remove requirement to use a predefined API version. Now you can use any valid API version string. ([#737](https://github.com/Shopify/shopify_python_api/pull/737))
diff --git a/README.md b/README.md
index 6226bcb1..cadda24e 100644
--- a/README.md
+++ b/README.md
@@ -66,10 +66,12 @@ pip install --upgrade ShopifyAPI
     api_version = '2024-07'
     state = binascii.b2a_hex(os.urandom(15)).decode("utf-8")
     redirect_uri = "http://myapp.com/auth/shopify/callback"
+    # `scope` should be omitted if provided by app's TOML
     scopes = ['read_products', 'read_orders']
 
     newSession = shopify.Session(shop_url, api_version)
-    auth_url = newSession.create_permission_url(scopes, redirect_uri, state)
+    # `scope` should be omitted if provided by app's TOML
+    auth_url = newSession.create_permission_url(redirect_uri, scopes, state)
     # redirect to auth_url
     ```
 
@@ -155,7 +157,7 @@ _Note: Your application must be public to test the billing process. To test on a
 > **⚠️ Note**: As of October 1, 2024, the REST Admin API is legacy:
 > - Public apps must migrate to GraphQL by February 2025
 > - Custom apps must migrate to GraphQL by April 2025
-> 
+>
 > For migration guidance, see [Shopify's migration guide](https://shopify.dev/docs/apps/build/graphql/migrate/new-product-model)
 
 It is recommended to have at least a basic grasp on the principles of the [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which is a port of rails/ActiveResource to Python and upon which this package relies heavily.
diff --git a/shopify/session.py b/shopify/session.py
index c3ec6d4b..561faacf 100644
--- a/shopify/session.py
+++ b/shopify/session.py
@@ -53,8 +53,11 @@ def __init__(self, shop_url, version=None, token=None, access_scopes=None):
         self.access_scopes = access_scopes
         return
 
-    def create_permission_url(self, scope, redirect_uri, state=None):
-        query_params = {"client_id": self.api_key, "scope": ",".join(scope), "redirect_uri": redirect_uri}
+    def create_permission_url(self, redirect_uri, scope=None, state=None):
+        query_params = {"client_id": self.api_key, "redirect_uri": redirect_uri}
+        # `scope` should be omitted if provided by app's TOML
+        if scope:
+            query_params["scope"] = ",".join(scope)
         if state:
             query_params["state"] = state
         return "https://%s/admin/oauth/authorize?%s" % (self.url, urllib.parse.urlencode(query_params))
diff --git a/shopify/version.py b/shopify/version.py
index 126c3ab4..dfb0b4e4 100644
--- a/shopify/version.py
+++ b/shopify/version.py
@@ -1 +1 @@
-VERSION = "12.7.0"
+VERSION = "12.7.1"


Test patch:
diff --git a/test/session_test.py b/test/session_test.py
index d7cd5c3d..8d73e293 100644
--- a/test/session_test.py
+++ b/test/session_test.py
@@ -86,51 +86,69 @@ def test_temp_works_without_currently_active_session(self):
         self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", assigned_site)
         self.assertEqual("https://none/admin/api/unstable", shopify.ShopifyResource.site)
 
-    def test_create_permission_url_returns_correct_url_with_single_scope_and_redirect_uri(self):
+    def test_create_permission_url_returns_correct_url_with_redirect_uri(self):
+        shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+        session = shopify.Session("http://localhost.myshopify.com", "unstable")
+        permission_url = session.create_permission_url("my_redirect_uri.com")
+        self.assertEqual(
+            "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+            self.normalize_url(permission_url),
+        )
+
+    def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope(self):
         shopify.Session.setup(api_key="My_test_key", secret="My test secret")
         session = shopify.Session("http://localhost.myshopify.com", "unstable")
         scope = ["write_products"]
-        permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
+        permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
         self.assertEqual(
             "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products",
             self.normalize_url(permission_url),
         )
 
-    def test_create_permission_url_returns_correct_url_with_dual_scope_and_redirect_uri(self):
+    def test_create_permission_url_returns_correct_url_with_redirect_uri_and_dual_scope(self):
         shopify.Session.setup(api_key="My_test_key", secret="My test secret")
         session = shopify.Session("http://localhost.myshopify.com", "unstable")
         scope = ["write_products", "write_customers"]
-        permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
+        permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
         self.assertEqual(
             "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products%2Cwrite_customers",
             self.normalize_url(permission_url),
         )
 
-    def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_uri(self):
+    def test_create_permission_url_returns_correct_url_with_redirect_uri_and_empty_scope(self):
         shopify.Session.setup(api_key="My_test_key", secret="My test secret")
         session = shopify.Session("http://localhost.myshopify.com", "unstable")
         scope = []
-        permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
+        permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+        self.assertEqual(
+            "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+            self.normalize_url(permission_url),
+        )
+
+    def test_create_permission_url_returns_correct_url_with_redirect_uri_and_state(self):
+        shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+        session = shopify.Session("http://localhost.myshopify.com", "unstable")
+        permission_url = session.create_permission_url("my_redirect_uri.com", state="mystate")
         self.assertEqual(
-            "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=",
+            "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&state=mystate",
             self.normalize_url(permission_url),
         )
 
-    def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_uri_and_state(self):
+    def test_create_permission_url_returns_correct_url_with_redirect_uri_empty_scope_and_state(self):
         shopify.Session.setup(api_key="My_test_key", secret="My test secret")
         session = shopify.Session("http://localhost.myshopify.com", "unstable")
         scope = []
-        permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate")
+        permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope, state="mystate")
         self.assertEqual(
-            "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=&state=mystate",
+            "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&state=mystate",
             self.normalize_url(permission_url),
         )
 
-    def test_create_permission_url_returns_correct_url_with_single_scope_and_redirect_uri_and_state(self):
+    def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope_and_state(self):
         shopify.Session.setup(api_key="My_test_key", secret="My test secret")
         session = shopify.Session("http://localhost.myshopify.com", "unstable")
         scope = ["write_customers"]
-        permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate")
+        permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope, state="mystate")
         self.assertEqual(
             "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_customers&state=mystate",
             self.normalize_url(permission_url),