Skip to content

Commit c7d1598

Browse files
author
Laura
authored
Initial app setup (#1)
1 parent be8310e commit c7d1598

13 files changed

Lines changed: 395 additions & 2 deletions

File tree

.env.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## DevCycle SDK Key ##
2+
# This can be found in the DevCycle dashboard Settings under "Environments & Keys"
3+
DEVCYCLE_SERVER_SDK_KEY="<YOUR_SDK_KEY>"

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,25 @@
1-
# example-python
2-
An example app built using the DevCycle Python SDK
1+
# DevCycle Python Server SDK Example App
2+
An example app built using the [DevCycle Python Server SDK](https://docs.devcycle.com/sdk/server-side-sdks/python/)
3+
4+
## Requirements.
5+
6+
Python 3.7+ and Django 4.2+
7+
8+
## Running the Example
9+
### Setup
10+
11+
* Create a `.env` file and set `DEVCYCLE_SERVER_SDK_KEY` to your Environment's SDK Key.\
12+
You can find this under [Settings > Environments](https://app.devcycle.com/r/environments) on the DevCycle dashboard.
13+
[Learn more about environments](https://docs.devcycle.com/essentials/environments).
14+
* Run `python3 -m pip install -r requirements.txt` in the project directory to install dependencies. You may need to run `pip` with root permission: `sudo pip install -r requirements.txt`
15+
* Run `python3 manage.py migrate` to apply migrations
16+
17+
### Development
18+
19+
`python3 manage.py runserver`
20+
21+
The server will start on port 8000. You can access the example app at http://localhost:8000.
22+
23+
## Documentation
24+
For more information about using the DevCycle Python Server SDK, see [the documentation](https://docs.devcycle.com/sdk/server-side-sdks/python/)
25+

hello_togglebot/__init__.py

Whitespace-only changes.

hello_togglebot/asgi.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
ASGI config for hello_togglebot project.
3+
4+
It exposes the ASGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
8+
"""
9+
10+
import os
11+
12+
from django.core.asgi import get_asgi_application
13+
14+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hello_togglebot.settings')
15+
16+
application = get_asgi_application()

hello_togglebot/devcycle.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.conf import settings
2+
from django.core.exceptions import ImproperlyConfigured
3+
from devcycle_python_sdk import (
4+
DevCycleLocalClient,
5+
DevCycleLocalOptions,
6+
)
7+
8+
try:
9+
devcycle_sdk_key = settings.DEVCYCLE_SERVER_SDK_KEY
10+
except AttributeError:
11+
raise ImproperlyConfigured("Please set DEVCYCLE_SERVER_SDK_KEY in .env")
12+
13+
# Create an options object to do custom configurations, or use the defaults
14+
options = DevCycleLocalOptions()
15+
16+
# Initialize the SDK singleton once here - it will be captured in the closure below
17+
devcycle_client = DevCycleLocalClient(devcycle_sdk_key, options)
18+
19+
def get_devcycle_client():
20+
return devcycle_client

hello_togglebot/middleware.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from devcycle_python_sdk.models.user import DevCycleUser
2+
from .devcycle import get_devcycle_client
3+
4+
def devcycle_middleware(get_response):
5+
"""
6+
This middleware adds the DevCycle client to the request object passed to
7+
all views as `request.devcycle`.
8+
"""
9+
def middleware(request):
10+
# all client functions require user data to be an instance of the DevCycleUser class
11+
request.user = DevCycleUser(
12+
user_id='user123',
13+
email='jane.doe@example.ca',
14+
country='CA'
15+
)
16+
request.devcycle = get_devcycle_client()
17+
return get_response(request)
18+
19+
return middleware

hello_togglebot/settings.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
Django settings for hello_togglebot project.
3+
4+
Generated by 'django-admin startproject' using Django 5.0.1.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/5.0/topics/settings/
8+
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/5.0/ref/settings/
11+
"""
12+
13+
from os import environ
14+
from pathlib import Path
15+
from dotenv import load_dotenv
16+
17+
load_dotenv()
18+
19+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
20+
BASE_DIR = Path(__file__).resolve().parent.parent
21+
22+
23+
# Quick-start development settings - unsuitable for production
24+
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
25+
26+
# SECURITY WARNING: keep the secret key used in production secret!
27+
SECRET_KEY = 'django-insecure-$sc_)$u!8^ryitgtejjf&-y3nwz2grovp88m8h-(c73m=2g!q%'
28+
29+
# SECURITY WARNING: don't run with debug turned on in production!
30+
DEBUG = True
31+
32+
ALLOWED_HOSTS = []
33+
34+
35+
# Application definition
36+
37+
INSTALLED_APPS = [
38+
'django.contrib.admin',
39+
'django.contrib.auth',
40+
'django.contrib.contenttypes',
41+
'django.contrib.sessions',
42+
'django.contrib.messages',
43+
'django.contrib.staticfiles',
44+
]
45+
46+
MIDDLEWARE = [
47+
'django.middleware.security.SecurityMiddleware',
48+
'django.contrib.sessions.middleware.SessionMiddleware',
49+
'django.middleware.common.CommonMiddleware',
50+
'django.middleware.csrf.CsrfViewMiddleware',
51+
'django.contrib.auth.middleware.AuthenticationMiddleware',
52+
'django.contrib.messages.middleware.MessageMiddleware',
53+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
54+
# Initialize the DevCycle client in the middleware
55+
'hello_togglebot.middleware.devcycle_middleware',
56+
]
57+
58+
ROOT_URLCONF = 'hello_togglebot.urls'
59+
60+
TEMPLATES = [
61+
{
62+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
63+
'DIRS': [],
64+
'APP_DIRS': True,
65+
'OPTIONS': {
66+
'context_processors': [
67+
'django.template.context_processors.debug',
68+
'django.template.context_processors.request',
69+
'django.contrib.auth.context_processors.auth',
70+
'django.contrib.messages.context_processors.messages',
71+
],
72+
},
73+
},
74+
]
75+
76+
WSGI_APPLICATION = 'hello_togglebot.wsgi.application'
77+
78+
79+
# Database
80+
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
81+
82+
DATABASES = {
83+
'default': {
84+
'ENGINE': 'django.db.backends.sqlite3',
85+
'NAME': BASE_DIR / 'db.sqlite3',
86+
}
87+
}
88+
89+
90+
# Password validation
91+
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
92+
93+
AUTH_PASSWORD_VALIDATORS = [
94+
{
95+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
96+
},
97+
{
98+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
99+
},
100+
{
101+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
102+
},
103+
{
104+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
105+
},
106+
]
107+
108+
109+
# Internationalization
110+
# https://docs.djangoproject.com/en/5.0/topics/i18n/
111+
112+
LANGUAGE_CODE = 'en-us'
113+
114+
TIME_ZONE = 'UTC'
115+
116+
USE_I18N = True
117+
118+
USE_TZ = True
119+
120+
121+
# Static files (CSS, JavaScript, Images)
122+
# https://docs.djangoproject.com/en/5.0/howto/static-files/
123+
124+
STATIC_URL = 'static/'
125+
126+
# Default primary key field type
127+
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
128+
129+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
130+
131+
# DevCycle Settings
132+
DEVCYCLE_SERVER_SDK_KEY = environ["DEVCYCLE_SERVER_SDK_KEY"]

hello_togglebot/urls.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
URL configuration for hello_togglebot project.
3+
4+
The `urlpatterns` list routes URLs to views. For more information please see:
5+
https://docs.djangoproject.com/en/5.0/topics/http/urls/
6+
Examples:
7+
Function views
8+
1. Add an import: from my_app import views
9+
2. Add a URL to urlpatterns: path('', views.home, name='home')
10+
Class-based views
11+
1. Add an import: from other_app.views import Home
12+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13+
Including another URLconf
14+
1. Import the include() function: from django.urls import include, path
15+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16+
"""
17+
import sys
18+
from django.urls import path
19+
from .views import home_page, get_variables
20+
from .utils import log_variation
21+
22+
urlpatterns = [
23+
path("", home_page, name="home"),
24+
path("variables", get_variables, name="variables"),
25+
]
26+
27+
is_runserver = any(arg.casefold() == "runserver" for arg in sys.argv)
28+
if (is_runserver):
29+
print('Starting development server at http://127.0.0.1:8000/')
30+
print('Quit the server with CONTROL-C.')
31+
log_variation()
32+

hello_togglebot/utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import sys
2+
import time
3+
from devcycle_python_sdk.models.user import DevCycleUser
4+
from .devcycle import get_devcycle_client
5+
6+
"""
7+
Since this is used outside of a request context, we define a service user.
8+
This can contian properties unique to this service, and allows you to target
9+
services in the same way you would target app users.
10+
"""
11+
SERVICE_USER = DevCycleUser(
12+
user_id='api-service'
13+
)
14+
15+
"""
16+
Log the current DevCycle variation to the console.
17+
"""
18+
def log_variation():
19+
devcycle_client = get_devcycle_client()
20+
21+
def render_frame(idx=0):
22+
timeout = 500
23+
24+
if devcycle_client.is_initialized():
25+
features = devcycle_client.all_features(SERVICE_USER)
26+
variation_name = features['hello-togglebot'].variationName if 'hello-togglebot' in features else 'Default'
27+
28+
wink = devcycle_client.variable_value(SERVICE_USER, 'togglebot-wink', False)
29+
speed = devcycle_client.variable_value(SERVICE_USER, 'togglebot-speed', 'off')
30+
31+
spin_chars = "◟◜◝◞" if speed == 'slow' else "◜◠◝◞◡◟"
32+
spinner = '○' if speed == 'off' else spin_chars[idx]
33+
idx = (idx + 1) % len(spin_chars)
34+
35+
face = '(- ‿ ○)' if wink else '(○ ‿ ○)'
36+
37+
frame = '{} Serving variation: {} {}'.format(spinner, variation_name, face)
38+
color = 'rainbow' if speed == 'surprise' else 'blue'
39+
40+
write_to_console(frame, color)
41+
42+
timeout = 100 if speed in ['fast', 'surprise', 'off-axis'] else 500
43+
44+
time.sleep(timeout / 1000)
45+
render_frame(idx)
46+
47+
try:
48+
print('{}------------------------------------------{}'.format(COLORS['blue'], END_CHAR))
49+
render_frame()
50+
except KeyboardInterrupt:
51+
sys.stdout.write('\n')
52+
sys.exit(0)
53+
54+
COLORS = {}
55+
COLORS['red'] = '\033[91m'
56+
COLORS['green'] = '\033[92m'
57+
COLORS['yellow'] = '\033[93m'
58+
COLORS['blue'] = '\033[94m'
59+
COLORS['magenta'] = '\033[95m'
60+
END_CHAR = '\033[0m'
61+
62+
def add_color(text, color):
63+
colors = {}
64+
colors.update(COLORS)
65+
colors['rainbow'] = '\033[38;5;{}m'.format(int(time.time() * 1000) % 230)
66+
67+
if color in colors:
68+
return colors[color] + text + END_CHAR
69+
else:
70+
return text
71+
72+
def write_to_console(frame, color):
73+
frame = add_color(frame, color)
74+
sys.stdout.write('\r\x1b[K' + frame)

hello_togglebot/views.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from django.http import HttpResponse
2+
import logging
3+
4+
logger = logging.getLogger(__name__)
5+
6+
7+
def home_page(request):
8+
step = request.devcycle.variable_value(request.user, "example-text", "default")
9+
10+
header = ""
11+
body = ""
12+
if step == "step-1":
13+
header = "Welcome to DevCycle's example app."
14+
body = "If you got here through the onboarding flow, just follow the instructions to change and create new Variations and see how the app reacts to new Variable values."
15+
elif step == "step-2":
16+
header = "Great! You've taken the first step in exploring DevCycle."
17+
body = "You've successfully toggled your very first Variation. You are now serving a different value to your users and you can see how the example app has reacted to this change. Next, go ahead and create a whole new Variation to see what else is possible in this app."
18+
elif step == "step-3":
19+
header = "You're getting the hang of things."
20+
body = "By creating a new Variation with new Variable values and toggling it on for all users, you've already explored the fundamental concepts within DevCycle. There's still so much more to the platform, so go ahead and complete the onboarding flow and play around with the feature that controls this example in your dashboard."
21+
else:
22+
header = "Welcome to DevCycle's example app."
23+
body = "If you got to the example app on your own, follow our README guide to create the Feature and Variables you need to control this app in DevCycle."
24+
25+
response = '<h2>{header}</h2><p>{body}</p><p><a href="/variables">All Variables</a></p>'.format(header=header, body=body)
26+
return HttpResponse(response)
27+
28+
def get_variables(request):
29+
variables = request.devcycle.all_variables(request.user)
30+
response = ''
31+
for key, variable in variables.items():
32+
response += '<li><strong>{key}</strong>: {value}</li>'.format(key=key, value=variable.value)
33+
return HttpResponse('<h2>Variables</h2><ul>' + response + '</ul><p><a href="/">Home</a></p>')

0 commit comments

Comments
 (0)