Python wrapper around Duckling's HTTP API with optional local server management.
Project description
Qwackling
qwackling is a Python client for Duckling's HTTP API.
The most important thing to understand is this:
- This package is the Python client.
- Duckling itself is a separate Haskell service.
- Building this Python package does not bundle, compile, or ship Duckling.
If you keep that mental model in mind, the rest of the workflow becomes much easier.
Read This README As Two Layers
This README focuses on:
- what the wrapper really is
- how it should be integrated
- how to develop and package it
If you want API-level usage details for the wrapper itself, read:
What This Project Actually Is
This repository contains a small Python package that sends HTTP requests to Duckling's /parse endpoint.
The code in this package:
- builds request payloads
- applies default parsing options like
locale,tz, andreftime - sends requests to a Duckling server with
requests - can optionally start a local Duckling process for development if you already have a Duckling checkout on disk
The code in this package does not:
- implement the Duckling parser itself
- compile Haskell code
- include the Duckling source tree in the wheel or sdist
- make Duckling magically available after
pip install
In other words, this package is a wrapper around Duckling, not a replacement for Duckling.
Architecture At A Glance
Your Python app
|
v
qwackling
|
v
HTTP POST /parse
|
v
Duckling server (separate process)
There are two supported ways to supply that Duckling server:
- Run Duckling as its own long-lived service and point this wrapper at it.
- Use
DucklingWrapper.start_server(...)to launch a local Duckling process from an existing Duckling checkout during development or tests.
For production, option 1 is the cleaner model.
What Gets Packaged
When you run:
python -m build --no-isolation
the generated artifacts in dist/ contain the Python package only:
qwacklingduckling_wrappercompatibility aliases- packaging metadata
- Python dependency declarations such as
requests
They do not contain:
duckling-example-exe- the Duckling git repository
- the Haskell
stacktoolchain - a prebuilt embedded parser binary
That behavior comes directly from pyproject.toml, which packages the Python code under src/qwackling/ and also ships a small duckling_wrapper compatibility alias.
What start_server(...) Really Does
DucklingWrapper.start_server(...) is a convenience for local development.
It does not download or build Duckling for you.
It expects:
- a Duckling source checkout already exists somewhere on disk
stackis installed and usableduckling-example-execan be launched from that checkout
What it actually does is:
- set the
PORTenvironment variable - run
stack exec duckling-example-exein the Duckling checkout directory - poll the configured
/parseendpoint until the server responds
So start_server(...) is "process management for an existing Duckling checkout", not "install Duckling automatically".
Choose Your Integration Model
Option 1: Existing Duckling service
Use this when:
- your main project already runs backend services
- you want cleaner deployment boundaries
- you want one Duckling instance shared by multiple workers or apps
- you are deploying to staging or production
Example:
from qwackling import DucklingWrapper
wrapper = DucklingWrapper(host="127.0.0.1", port=8000)
results = wrapper.parse("tomorrow at 8pm")
print(results)
This is the recommended production model.
Option 2: Start Duckling from Python for local dev/tests
Use this when:
- you are developing locally
- you want a self-contained test harness
- you already have a Duckling checkout available
Example:
from qwackling import DucklingWrapper
with DucklingWrapper(port=8000) as wrapper:
wrapper.start_server("./duckling")
results = wrapper.parse("tomorrow at 8pm")
print(results)
This is convenient, but it still depends on a separate Duckling checkout and Haskell toolchain being present.
Install The Python Wrapper
Runtime install
pip install .
Development install
With pip:
python -m pip install -e ".[dev]"
With uv:
uv sync --extra dev
With the helper script:
./scripts/manage_wrapper.sh install-dev
Build Duckling Locally
You only need this if you want to run Duckling yourself, especially through start_server(...).
On Ubuntu or Debian:
sudo apt-get update
sudo apt-get install -y curl pkg-config libpcre3-dev
curl -sSL https://get.haskellstack.org/ | sh
Then from this package directory:
./scripts/manage_wrapper.sh build-duckling
That helper script:
- clones
https://github.com/facebook/duckling.gitinto./ducklingif needed - runs
stack buildinside that checkout
The script lives at scripts/manage_wrapper.sh.
Or run with docker:
sudo docker run -p 8000:8000 rasa/duckling
Quick Start
Call an already-running Duckling service
from qwackling import DucklingWrapper
wrapper = DucklingWrapper(host="127.0.0.1", port=8000)
print(wrapper.parse("tomorrow at 8pm"))
Start a local Duckling checkout from Python
from qwackling import DucklingWrapper
with DucklingWrapper(port=8000) as wrapper:
wrapper.start_server("./duckling")
print(wrapper.parse("tomorrow at 8pm"))
For deeper wrapper API usage, parameter meanings, config helpers, and more examples, see:
Configuration
The wrapper lets you set stable defaults once and then reuse them for future calls.
from datetime import datetime
from zoneinfo import ZoneInfo
from qwackling import DucklingWrapper, to_epoch_millis
wrapper = DucklingWrapper().config(
locale="en_US",
tz="Asia/Kolkata",
reftime=to_epoch_millis(
datetime(2026, 4, 8, 10, 0, tzinfo=ZoneInfo("Asia/Kolkata"))
),
dims=["time"],
)
results = wrapper.parse("tomorrow at 8pm")
Default values:
locale="en_US"lang=Nonedims=Nonelatent=Falsetz=Nonereftime=Nonerequest_timeout=10.0startup_retries=30startup_wait_seconds=1.0
Notes:
tzshould be an IANA timezone such asAsia/KolkataorUTCreftimemust be Unix epoch millisecondsto_epoch_millis(...)requires a timezone-awaredatetime
How To Integrate This Into Your Main Project
If your main project needs natural-language date/time parsing, treat Duckling and this wrapper as two different dependencies:
- your Python app dependency:
qwackling - your runtime parser service: Duckling
Recommended production integration
In a main backend project, the cleanest pattern is usually:
- deploy Duckling as a separate service or sidecar
- configure its host and port through environment variables
- instantiate
DucklingWrapperonce in your app - call
parse(...)wherever you need text-to-time extraction
Example:
import os
from qwackling import DucklingWrapper
duckling = DucklingWrapper(
host=os.getenv("DUCKLING_HOST", "127.0.0.1"),
port=int(os.getenv("DUCKLING_PORT", "8000")),
).config(
locale="en_US",
tz="UTC",
dims=["time"],
)
def parse_natural_language_datetime(text: str):
return duckling.parse(text)
Why this is usually the best choice:
- your app and parser can scale independently
- startup is simpler and more predictable
- failures are easier to observe
- deployments do not require bundling Haskell into your Python package
What this means in practice
If your main project is a Python app or API service, a good setup is usually:
- add
qwacklingas a Python dependency - run Duckling separately as infrastructure
- keep Duckling host and port in config or environment variables
- create one wrapper instance at app startup
- call
parse(...)from your domain code
That keeps the Python package lightweight and keeps the Haskell runtime out of your Python build process.
Good local development pattern
For local development, you can either:
- run Duckling separately in another terminal, or
- let tests/dev scripts call
start_server(...)
That keeps production simple while still making local iteration easy.
Good testing pattern
For unit tests in your main project:
- mock the wrapper or the HTTP boundary
- avoid depending on a real Duckling process unless the test is specifically integration-level
For integration tests:
- start a real Duckling instance
- use fixed
reftimevalues so relative expressions stay deterministic
What To Put In Your Main Project Docs
If you adopt this package in another repo, document it in these terms:
- "We use
qwacklingas the Python client." - "We run Duckling as a separate service."
- "The Python dependency does not include the Duckling executable."
That one clarification prevents most setup confusion.
Common Misunderstanding
If someone does this:
pip install qwackling
python app.py
and expects parsing to work immediately, they will usually get a connection error.
That does not mean the Python package is broken.
It usually means no Duckling server is listening yet.
The correct fix is one of:
- start Duckling separately and point the wrapper at it
- build a local Duckling checkout and call
start_server(...)
Wrapper Development
If you are developing this wrapper package itself, think of the work in three separate areas:
- Python wrapper code
- optional local Duckling runtime for integration-style workflows
- packaging and publishing
Python wrapper code
The Python package source lives under:
src/qwackling/src/duckling_wrapper/for the compatibility alias
That is the code that gets built into the Python distribution.
Tests live under:
tests/
Local Duckling runtime
The local Duckling checkout is only a helper for development and manual/integration testing.
It is not part of the Python package source tree even if it exists at:
./duckling
If that directory exists, it is a runtime helper checkout, not packaged library code.
Development install
For active wrapper development:
python -m pip install -e ".[dev]"
Or:
uv sync --extra dev
Development workflow
A practical flow for wrapper development is:
- edit code under
src/qwackling/ - run unit tests
- if needed, build Duckling locally for manual validation
- build the Python package
- install that package into another project for verification
Build
Create source and wheel distributions:
python -m build --no-isolation
Or:
./scripts/manage_wrapper.sh build
Artifacts are written to dist/, for example:
dist/qwackling-0.1.1-py3-none-any.whl
dist/qwackling-0.1.1.tar.gz
What the build output contains
The package build contains:
- Python wrapper source
- package metadata
- declared Python dependencies
The package build does not contain:
- Duckling source code
- a compiled Duckling executable
- the Haskell toolchain
- a local
./ducklingcheckout
That separation is intentional.
Tests
Run the unit tests after a dev install:
python -m pytest -c pyproject.toml tests
Or:
./scripts/manage_wrapper.sh test
The current tests cover:
- wrapper defaults
- per-call overrides
- payload generation
- clearing optional config
- local server startup success and failure paths
- timezone-aware datetime conversion
Unit tests validate wrapper behavior. They do not prove that Duckling is installed on a target machine.
If you want end-to-end confidence, run an integration flow against a real Duckling server as a separate step.
Integrate From Another Project
Add as a local path dependency
With uv:
uv add /absolute/path/to/qwackling
With pip:
pip install /absolute/path/to/qwackling
Install from a built wheel
python -m build --no-isolation
pip install /absolute/path/to/qwackling/dist/qwackling-0.1.1-py3-none-any.whl
Editable install while actively developing the wrapper
pip install -e /absolute/path/to/qwackling
This is the best option when your main project and the wrapper are evolving together locally.
Publish To PyPI
Build distributions:
python -m build --no-isolation
Validate them:
python -m twine check dist/*
Upload to TestPyPI first:
python -m twine upload --repository testpypi dist/*
Verify installation:
pip install --index-url https://test.pypi.org/simple/ qwackling
Publish to PyPI:
python -m twine upload dist/*
Helper Script Commands
./scripts/manage_wrapper.sh all
./scripts/manage_wrapper.sh install-dev
./scripts/manage_wrapper.sh build-duckling
./scripts/manage_wrapper.sh test
./scripts/manage_wrapper.sh build
./scripts/manage_wrapper.sh clean
all is a convenience workflow for wrapper development. It installs dev dependencies, builds Duckling locally, runs tests, and builds the Python package.
Short Version
If you only remember four things, remember these:
qwacklingis a Python client, not the parser itself.python -m buildpackages only the Python wrapper.- Duckling must exist as a separate running service or local checkout.
- For a main production app, run Duckling separately and use this package as the client.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qwackling-0.1.1.tar.gz.
File metadata
- Download URL: qwackling-0.1.1.tar.gz
- Upload date:
- Size: 18.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df50b0ac96f741110967563781a562bc0e7ebeaaad578871858deaafbfd41e56
|
|
| MD5 |
671b030a774e10192cd3aeba0f3f5526
|
|
| BLAKE2b-256 |
daa0a39c690f0f5eee62b7d53bd13b6508a726a53ba88f751b012700c09e2d76
|
File details
Details for the file qwackling-0.1.1-py3-none-any.whl.
File metadata
- Download URL: qwackling-0.1.1-py3-none-any.whl
- Upload date:
- Size: 13.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59993e8a4601080eb6142eb05f371872dd346902545b7061528cf70d89080e54
|
|
| MD5 |
0aa62ceef6c5ac950a7f748cb171bd77
|
|
| BLAKE2b-256 |
96dbea5529f0b4f9ca7112b5335fcdc750adaa46fc9acb544d06a70fd34c6d01
|