Skip to content

Commit 4976efe

Browse files
author
chiku
committed
Introduce a dynamic addon imp registry to support Foreign Addon Imps
A Foreign Addon Imp is an addon implementation that is put outside of gravyvalet codebase offered as a Django application. A collection of Foreign Addon Imps is distributed as a Django project. AddonRegistry class is the dynamic addon imp registry that holds the addon imps in use. Even the built-in addon implementation needs to be registered to be used, by adding the name and imp number pair to `app.settings.ADDON_IMPS` configuration. `AddonImpNumbers` enum is discarded. In addition, AddonImpRegistry class replaces the public interface of `addon_service.common.known_imps` module. Foreign Addon Imps needs to be registered to `ADDON_IMPS` too. Moreover, they needs to be registered to `INSTALLED_APPS`.
1 parent 8fd6d61 commit 4976efe

File tree

22 files changed

+1275
-132
lines changed

22 files changed

+1275
-132
lines changed

FOREIGN_ADDON_IMP_DEVELOPMENT.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Foreign Addon Imp Development Guide
2+
3+
This guide explains how to develop Foreign Addon Imps for gravyvalet, allowing you to create custom integrations that can be distributed as standalone Python packages.
4+
5+
## Overview
6+
7+
Foreign Addon Imps are Django apps that extend gravyvalet's functionality by implementing new external service integrations. They can be developed independently and distributed as regular Python packages.
8+
9+
## Requirements
10+
11+
- Python 3.9+
12+
- Django 4.2+
13+
- gravyvalet
14+
15+
## Development Steps
16+
17+
### Create Your Package Structure
18+
19+
Create a standard Python package structure:
20+
21+
```
22+
your_addon_imp_package/
23+
├── setup.py
24+
├── README.md
25+
├── your_addon_imp_app/
26+
│ ├── __init__.py
27+
│ ├── apps.py
28+
│ ├── your_imp.py # your AddonImp implementation
29+
│ └── static/
30+
│ └── {AppConfig.name}/ # typically, `your_addon_imp_package.your_addon_imp_app`
31+
│ └── icons/ # Place your addon's icon files here
32+
│ └── your_icon.svg
33+
```
34+
35+
A foreign addon imp package can include multiple foreign addon imps. To do so,
36+
just include multiple Django apps that behave as foreign addon imps.
37+
38+
### Implement Your Django App Config
39+
40+
Create `apps.py` with a class that inherits from `ForeignAddonImpConfig`:
41+
42+
```python
43+
from addon_toolkit.interfaces.foreign_addon_imp_config import ForeignAddonImpConfig
44+
from .your_imp import YourServiceImp
45+
46+
class YourAddonImpConfig(ForeignAddonImpConfig):
47+
name = "your_addon_imp_package.your_addon_imp_app"
48+
49+
@property
50+
def imp(self):
51+
"""Return your AddonImp implementation class."""
52+
return YourServiceImp
53+
54+
@property
55+
def addon_imp_name(self):
56+
"""Return the unique name for your addon imp.
57+
58+
IMPORTANT: This name MUST be unique across all addon imps in
59+
gravyvalet installation. Users will reference this name in their
60+
ADDON_IMPS configuration.
61+
"""
62+
return "YOUR_ADDON_IMP_APP_NAME"
63+
```
64+
65+
### Implement Your AddonImp
66+
67+
Create your AddonImp implementation based on the type of service:
68+
69+
```python
70+
from addon_toolkit.imp import AddonImp
71+
from addon_toolkit.imp_subclasses.storage import StorageAddonImp
72+
73+
class YourServiceImp(StorageAddonImp):
74+
# Implement required methods and properties
75+
# See addon_toolkit documentation for details
76+
pass
77+
```
78+
79+
The modules under the `addon_imps` package are good examples to
80+
implement this part.
81+
82+
### Choose a Unique Addon Imp Name and Document the name
83+
84+
**CRITICAL**: Your `addon_imp_name` must be unique to avoid conflicts.
85+
86+
Before choosing a name, check built-in addon imp names in
87+
`addon_service.common.known_imps.KnownAddonImps` and avoid to use the
88+
names enumerated.
89+
90+
Document the name clearly so users know exactly what to use. Since users
91+
can use the package name of the addon imp application instaed of
92+
`addon_imp_name` value, document the package name too is a recommended
93+
manner.
94+
95+
### Adding Icons for Your Addon Imp
96+
97+
Foreign addon imps can provide custom icons that will be available in
98+
the gravyvalet admin interface.
99+
100+
#### Icon Directory Convention
101+
102+
Place your icon files in the `static/{AppConfig.name}/icons/` directory
103+
within your addon imp app:
104+
105+
```
106+
your_addon_imp_app/
107+
├── static/
108+
│ └── {AppConfig.name} # typically, your_addon_imp_package.your_addon_imp_app/
109+
│ └── icons/
110+
│ ├── your_service.svg
111+
│ ├── your_service.png
112+
│ └── your_service_alt.jpg
113+
```
114+
115+
#### Supported Formats
116+
117+
- SVG (recommended for scalability)
118+
- PNG
119+
- JPG/JPEG
120+
121+
### Package and Distribute
122+
123+
Create a `setup.py` for your package:
124+
125+
```python
126+
from setuptools import setup, find_packages
127+
128+
setup(
129+
name="your-addon-imp-package",
130+
version="1.0.0",
131+
packages=find_packages(),
132+
include_package_data=True, # Important for including static files
133+
install_requires=[
134+
"django>=4.2",
135+
],
136+
package_data={
137+
'your_addon_imp_app': [
138+
'static/your_addon_imp_package.your_addon_imp_app/icons/*', # Include icon files
139+
],
140+
},
141+
)
142+
```
143+
144+
## Installation and Usage
145+
146+
Users can install and use your foreign addon imps by:
147+
148+
1. Installing your package:
149+
```bash
150+
pip install your-addon-imp-package
151+
```
152+
153+
2. Adding it to Django's `INSTALLED_APPS`:
154+
```python
155+
INSTALLED_APPS = [
156+
# ... other apps
157+
"your_addon_imp_package.your_addon_imp_app",
158+
]
159+
```
160+
161+
3. Registering it in gravyvalet's `ADDON_IMPS`:
162+
```python
163+
ADDON_IMPS= {
164+
# ... other addons
165+
"YOUR_ADDON_IMP_APP_NAME": 5000, # Use a unique (eg. ID >= 5000)
166+
}
167+
```

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,56 @@ To configure OAuth addons:
5656
4. There fill your client id and client secret (instructions to obtain them are [here](./services_setup_doc/README.md))
5757
5. Now you should be able to connect these addons according to existing user flows (in ordinary osf app)
5858

59+
## ...use foreign addon imps
60+
61+
Foreign addon imps allow you to extend gravyvalet with additional integrations
62+
without modifying the core code.
63+
64+
To use foreign addon imps:
65+
66+
1. Install the foreign addon imp package(s):
67+
```bash
68+
pip install foreign-addon-imp-package-you-want
69+
```
70+
71+
2. Add the foreign addon imp(s) to `INSTALLED_APPS` in your Django settings:
72+
```python
73+
INSTALLED_APPS = [
74+
# ... existing apps ...
75+
'foreign_addon_imp_package_you_want.app_name',
76+
# ...
77+
'addon_service',
78+
# ...
79+
]
80+
```
81+
82+
3. Register each foreign addon imp to `ADDON_IMPS` with a unique ID number:
83+
```python
84+
ADDON_IMPS = {
85+
# ... other addons ...
86+
"YOUR_ADDON_IMP_NAME": 5001, # Use a unique number not used by other addons
87+
}
88+
```
89+
90+
The name of each addon imp must be documented in the document of the foreign
91+
addon imp package. If 2 addon imp applications you want to use adopted identical
92+
names, use the package name instaed:
93+
94+
```python
95+
ADDON_IMPS = {
96+
# ... other addons ...
97+
'foreign_addon_imp_package_you_want.app_name': 5001,
98+
}
99+
```
100+
101+
The ID numbers must be:
102+
- Unique across all addon imps
103+
- Never changed once assigned (changing would break existing configurations)
104+
105+
4. Restart gravyvalet to load the new foreign addon imps
106+
107+
After these steps, the foreign addon imps will be available.
108+
59109
## ...configure a good environment
60110
see `app/env.py` for details on all environment variables used.
61111

addon_service/addon_imp/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import cached_property
44

55
from addon_service.addon_operation.models import AddonOperationModel
6-
from addon_service.common import known_imps
6+
from addon_service.common.known_imps import AddonImpRegistry
77
from addon_service.common.static_dataclass_model import StaticDataclassModel
88
from addon_toolkit import AddonImp
99

@@ -19,12 +19,12 @@ class AddonImpModel(StaticDataclassModel):
1919

2020
@classmethod
2121
def init_args_from_static_key(cls, static_key: str) -> tuple:
22-
return (known_imps.get_imp_by_name(static_key),)
22+
return (AddonImpRegistry.get_imp_by_name(static_key),)
2323

2424
@classmethod
2525
def iter_all(cls) -> typing.Iterator[typing.Self]:
26-
for _imp in known_imps.KnownAddonImps:
27-
yield cls(_imp.value)
26+
for _imp in AddonImpRegistry.get_all_addon_imps():
27+
yield cls(_imp)
2828

2929
@property
3030
def static_key(self) -> str:
@@ -35,7 +35,7 @@ def static_key(self) -> str:
3535

3636
@cached_property
3737
def name(self) -> str:
38-
return known_imps.get_imp_name(self.imp_cls)
38+
return AddonImpRegistry.get_imp_name(self.imp_cls)
3939

4040
@cached_property
4141
def imp_docstring(self) -> str:

addon_service/admin/__init__.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from django.contrib import admin
22

33
from addon_service import models
4-
from addon_service.common import known_imps
54
from addon_service.common.credentials_formats import CredentialsFormats
5+
from addon_service.common.known_imps import AddonImpRegistry
66
from addon_service.common.service_types import ServiceTypes
77
from addon_service.external_service.computing.models import ComputingSupportedFeatures
88
from addon_service.external_service.storage.models import StorageSupportedFeatures
9+
from addon_toolkit.interfaces.citation import CitationAddonImp
10+
from addon_toolkit.interfaces.computing import ComputingAddonImp
11+
from addon_toolkit.interfaces.link import LinkAddonImp
12+
from addon_toolkit.interfaces.storage import StorageAddonImp
913

1014
from ..external_service.citation.models import CitationSupportedFeatures
1115
from ..external_service.link.models import (
@@ -26,13 +30,15 @@ class ExternalStorageServiceAdmin(GravyvaletModelAdmin):
2630
)
2731
raw_id_fields = ("oauth2_client_config", "oauth1_client_config")
2832
enum_choice_fields = {
29-
"int_addon_imp": known_imps.StorageAddonImpNumbers,
3033
"int_credentials_format": CredentialsFormats,
3134
"int_service_type": ServiceTypes,
3235
}
3336
enum_multiple_choice_fields = {
3437
"int_supported_features": StorageSupportedFeatures,
3538
}
39+
dynamic_choice_fields = {
40+
"int_addon_imp": lambda: AddonImpRegistry.iter_by_type(StorageAddonImp),
41+
}
3642

3743

3844
@admin.register(models.ExternalCitationService)
@@ -45,13 +51,15 @@ class ExternalCitationServiceAdmin(GravyvaletModelAdmin):
4551
)
4652
raw_id_fields = ("oauth2_client_config", "oauth1_client_config")
4753
enum_choice_fields = {
48-
"int_addon_imp": known_imps.CitationAddonImpNumbers,
4954
"int_credentials_format": CredentialsFormats,
5055
"int_service_type": ServiceTypes,
5156
}
5257
enum_multiple_choice_fields = {
5358
"int_supported_features": CitationSupportedFeatures,
5459
}
60+
dynamic_choice_fields = {
61+
"int_addon_imp": lambda: AddonImpRegistry.iter_by_type(CitationAddonImp),
62+
}
5563

5664

5765
@admin.register(models.ExternalLinkService)
@@ -64,14 +72,16 @@ class ExternalLinkServiceAdmin(GravyvaletModelAdmin):
6472
)
6573
raw_id_fields = ("oauth2_client_config", "oauth1_client_config")
6674
enum_choice_fields = {
67-
"int_addon_imp": known_imps.LinkAddonImpNumbers,
6875
"int_credentials_format": CredentialsFormats,
6976
"int_service_type": ServiceTypes,
7077
}
7178
enum_multiple_choice_fields = {
7279
"int_supported_features": LinkSupportedFeatures,
7380
"int_supported_resource_types": SupportedResourceTypes,
7481
}
82+
dynamic_choice_fields = {
83+
"int_addon_imp": lambda: AddonImpRegistry.iter_by_type(LinkAddonImp),
84+
}
7585

7686

7787
@admin.register(models.ExternalComputingService)
@@ -84,13 +94,15 @@ class ExternalComputingServiceAdmin(GravyvaletModelAdmin):
8494
)
8595
raw_id_fields = ("oauth2_client_config",)
8696
enum_choice_fields = {
87-
"int_addon_imp": known_imps.ComputingAddonImpNumbers,
8897
"int_credentials_format": CredentialsFormats,
8998
"int_service_type": ServiceTypes,
9099
}
91100
enum_multiple_choice_fields = {
92101
"int_supported_features": ComputingSupportedFeatures,
93102
}
103+
dynamic_choice_fields = {
104+
"int_addon_imp": lambda: AddonImpRegistry.iter_by_type(ComputingAddonImp),
105+
}
94106

95107

96108
@admin.register(models.OAuth2ClientConfig)

0 commit comments

Comments
 (0)