Python
Getting Started with the Python SDK
Add prefab_cloud_python
to your package dependencies
# pyproject.toml
[tool.poetry.dependencies]
prefab-cloud-python = "^0.10.3"
Initialize Client
If you set PREFAB_API_KEY
as an environment variable, initializing the client is as easy as
import prefab_cloud_python
prefab_cloud_python.set_options(prefab_cloud_python.Options()) # reads PREFAB_API_KEY env var
Unless your options are configured to run using only local data, the client will attempt to connect to the remote CDN.
Special Considerations with Forking servers like Gunicorn that use workers
Webservers like gunicorn can be configured to either use threads or fork child process workers. When forking, the prefab client must be re-created in order to continue to fetch updated configuration.
# gunicorn configuration hook
def post_worker_init(worker):
prefab_cloud_python.reset_instance()
You may also do something like using uWSGI decorators
@uwsgidecorators.postfork
def post_fork():
prefab_cloud_python.reset_instance()
This clears the package-singleton client and on the next prefab_cloud_python.get_client()
it will be recreated with the options previously set with set_options()
Basic Usage
Defaults
Here we ask for the value of a config named max-jobs-per-second
, and we specify
10
as a default value if no value is available.
prefab_cloud_python.get_client().get("max-jobs-per-second", default=10) # => 10
If no default is provided, the default behavior is to raise a MissingDefaultException
.
# raises a `MissingDefaultException`
prefab_cloud_python.get_client().get("max-jobs-per-second")
Handling Undefined Configs
If you would prefer your application return None
instead of raising an error,
you can set on_no_default="RETURN_NONE"
when creating your Options object.
options = Options(
...
on_no_default="RETURN_NONE"
)
prefab_cloud_python.set_options(options)
prefab_cloud_python.get_client().get("max-jobs-per-second") # => None
Getting Started
Now create a config named my-first-int-config
in the Prefab UI. Set a default
value to 50 and sync your change to the API.
Add a feature flag named my-first-feature-flag
in the Prefab UI. Add boolean
variants of true
and false
. Set the inactive variant to false, make the flag
active and add a rule of type ALWAYS_TRUE
with the variant to serve as true
.
Remember to sync your change to the API.
config_key = "my-first-int-config"
print(config_key, prefab_cloud_python.get_client().get(config_key))
ff_key = "my-first-feature-flag"
print(config_key, prefab_cloud_python.get_client().enabled(ff_key))
Run the code above and you should see:
my-first-int-config 50
my-first-feature-flag true
Congrats! You're ready to rock!
Feature Flags
Feature flags become more powerful when we give the flag evaluation rules more information to work with.
We do this by providing a context for the current user (and/or team, request, etc)
context = {
"user": {
"key": 123,
"subscription_level": "pro",
"email": "bob@example.com"
},
"team": {
"key": 432,
},
"device": {
"key": "abcdef",
"mobile": False
}
}
result = prefab_cloud_python.get_client().enabled("my-first-feature-flag", context=context)
Feature flags don't have to return just true or false. You can get other data types using get
:
prefab_cloud_python.get_client().get("ff-with-string", default="default-string", context=context)
prefab_cloud_python.get_client().get("ff-with-int", default=5)
Thread-local context
To avoid having to pass a context explicitly to every call to get
or enabled
, it is possible to set a thread-local
context that will be evaluated as the default argument to context=
if none is given.
from prefab_cloud_python import Context
context = {
"user": {
"key": 123,
"subscription_level": "pro",
"email": "bob@example.com"
},
"team": {
"key": 432,
},
"device": {
"key": "abcdef",
"mobile": False
}
}
shared_context = Context(context)
Context.set_current(shared_context)
# with this set, the following two client calls are equivalent
result = prefab_cloud_python.get_client().enabled("my-first-feature-flag")
result = prefab_cloud_python.get_client().enabled("my-first-feature-flag", context=context)
Scoped context
It is also possible to scope a context for a particular block of code, without needing to set and unset the thread-local context
import prefab_cloud_python
from prefab_cloud_python import Client
context = {
"user": {
"key": 123,
"subscription_level": "pro",
"email": "bob@example.com"
},
"team": {
"key": 432,
},
"device": {
"key": "abcdef",
"mobile": False
}
}
with Client.scoped_context(context):
result1 = prefab_cloud_python.get_client().enabled("my-first-feature-flag")
result2 = prefab_cloud_python.get_client().enabled("my-first-feature-flag", context=context)
result1 == result2 #=> True
Logging
Prefab's Python Client upgrades provides a logging filter that can be plugged into logging
or structlogger
to provide dynamic log levels. The client assumes your loggers are initialized with the name of each module, ie logger = logging.get_logger(__name__)
Targeted Log Levels
You can use Targeting to change your log levels based on the current user/request/device context using our rules engine.
Log levels
To be as language agnostic as possible, Prefab
provides a standardized
subset of log levels that can be mapped to language-specific log levels.
The language-agnostic levels are the levels that should be set in the Prefab
UI or in your local overrides. The list below shows the mappings from Prefab
log levels to Python log levels
Prefab => Python
--------------------
debug => :debug
info => :info
warn => :warn
error => :error
fatal => :critical
Configuration for Standard Logging
In standard logging there are two steps
- Create an instance of
LoggerFilter
and configure it as a filter on the logging streamhandler - Set the root logger's loglevel to
logging.DEBUG
so that theLoggerFilter
will see all the log records
import logging
# basic logging setup - yours may vary
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # set to DEBUG so that LoggerFilter will see all log records
ch = logging.StreamHandler(sys.stdout)
ch.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
root_logger.addHandler(ch)
# key step - add the Prefab LoggerFilter to the StreamHandler
ch.addFilter(LoggerFilter())
Now your normal logging use cases eg
logger = logging.getLogger(__name__)
logger.debug("something")
can be controlled dynamically from Prefab
See example
Note the LoggerFilter has an overrideable logger_name
method so you can subclass and determine your own logger name as needed
Configuration for Structlogger
The configuration below is for a structlogger setup backed by the standard logger.
Using the structlog.stdlib.LoggerFactory()
ensures the logger instances have names. The initial two processors ensure the log name and level are available on the event dictionary.
import structlog
structlog.configure(
processors=[
structlog.stdlib.add_logger_name,
structlog.processors.add_log_level,
LoggerProcessor(),
structlog.processors.StackInfoRenderer(),
structlog.dev.set_exc_info,
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False),
structlog.dev.ConsoleRenderer(colors=True),
],
logger_factory=structlog.stdlib.LoggerFactory(), # Use Python's logging factory
wrapper_class=structlog.stdlib.BoundLogger
)
See example
Note the LoggerProcessor has an overrideable logger_name
method so you can subclass and determine your own logger name from elements on the logger record as needed.
Please contact us for help with your configuration if it varies from one of these standard cases.
Uvicorn Logging
Uvicorn will default to setting up it's own logging. If you'd like to use your own logging configuration, you can do so by passing log_config=None
as shown below
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, log_config=None)
# or
uvicorn.run("__main__:app", host="0.0.0.0", port=8000, log_config=None, reload=True)
Debugging
At this time, it's not possible to dynamically control the loglevel of the prefab client itself. Instead control the Prefab client's log level by changing the bootstrap_loglevel
in the Options
class at start up.
By default this level is set to Logging.WARNING
Testing
Specify LOCAL_ONLY
and use your config.yaml file.
prefab_cloud_python.set_options(Options(data_sources="LOCAL_ONLY")
prefab_cloud_python.get_client()...
Reference
Available Option
parameters
api_key
- your prefab.cloud SDK API keyprefab_api_url
- the API endpoint your API key has been created for (i.e.https://api.prefab.cloud
)prefab_datasources
- one of"ALL"
(default) or"LOCAL_ONLY"
, determines whether to fetch data from remote sources or use only local dataprefab_config_classpath_dir
- the directory from which to load locally defined configuration. This data will be overwritten by data pulled from remote sources. This value defaults to the root of your project (i.e."."
)prefab_config_override_dir
- the directory from which to load local override data. Any data found will be loaded overtop of data pulled from remote sources. This value defaults to your$HOME
directory.prefab_envs
- one or more environment names from which to load local configuration and overrides. See Local config and overrides below for additional information.on_no_default
- one of"RAISE"
(default) or"RETURN_NONE"
. This determines how the client behaves when a request for a config cannot find a value, and no default is supplied. These settings will, respectively, raise aMissingDefaultException
, or returnNone
.on_connection_failure
- one of"RETURN"
(default) or"RAISE"
. This determines what should happen if the connection to a remote datasource times out. These settings will, respectively, return whatever is in the local cache from the latest sync from the remote source, or else raise anInitializationTimeoutException
.collect_sync_interval
- how often to send telemetry to Prefab (seconds, defaults to 30)collect_evaluation_summaries
- send aggregate data about config and feaure flag evaluations, results (defaults to True) Evaluation Summary telemetry Implemented in v0.10+collect_logs
- send aggregate logger volume data to Prefab (defaults to True)context_upload_mode
- send context information to prefab. Values (from theOptions.ContextUploadMode
enum) areNONE
(don't send any context data),SHAPE_ONLY
to only send the schema of the contexts to prefab (field name, data types),PERIODIC_EXAMPLE
to send the data types AND the actual contexts being used to Prefab Context telemetry Implemented in v0.10+global_context
- an immutable global context to be used in all lookups. Use this for things like availability zone, machine type...on_ready_callback
- register a single method to be called when the client has loaded its first configuration and is ready for use