Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
4973dfc
remove extra fields in default generalsettings
caspycat Jun 19, 2019
7ab5e9f
update avail_from/to fields in generalSettings
caspycat Oct 14, 2019
58b93d1
add bean class and unit tests
caspycat Oct 15, 2019
20d5126
add readonly capabilities and warnings for bean
caspycat Oct 15, 2019
0b9b273
add data fields for all flavours of vehicles
caspycat Oct 15, 2019
1cadd6b
add data fields for all flavours of stops
caspycat Oct 15, 2019
786a7e8
remove constructor override in common vehicle
caspycat Oct 15, 2019
3908678
add data fields for all flavours of depots
caspycat Oct 17, 2019
641a859
add bean.get_full_default_data method and tests
caspycat Oct 17, 2019
c1907d6
add required_data_keys to bean
caspycat Oct 17, 2019
0448146
add basic bean serializers and tests
caspycat Oct 17, 2019
972fc27
add safe .get retrieval method on bean
caspycat Oct 17, 2019
4626c1e
add basic validators
caspycat Oct 17, 2019
d33d806
add get_full_results_data method
caspycat Oct 22, 2019
26c98b2
fixed wrong inheritance and remove print statement
caspycat Oct 22, 2019
521dda7
move exceptions directory to errors because reasons
caspycat Oct 22, 2019
d231b33
fix syntax error in validator
caspycat Oct 23, 2019
bab4f00
add old_name property to DashboardStop
caspycat Oct 23, 2019
3dd33f5
add stop deserializers
caspycat Oct 23, 2019
b4c8535
add stop repository
caspycat Oct 23, 2019
e31bdd0
add dashboardclient
caspycat Oct 23, 2019
636732b
hook validators into repositories
caspycat Oct 23, 2019
1cee7bf
add vehicle serializers
caspycat Oct 23, 2019
f1550e3
add vehicle repository and add to client
caspycat Oct 23, 2019
07bc74f
add vehicle deserializer
caspycat Oct 23, 2019
c56a588
fix wrong attribtue name in vehicle validation clause
caspycat Oct 23, 2019
e58ebb3
fix vehicle data attributes
caspycat Oct 23, 2019
9ecede5
add vehicle repository to dashboardclient
caspycat Oct 23, 2019
721e240
fix wrong target class in serializers
caspycat Oct 23, 2019
94ea8e0
delete old files
caspycat Oct 23, 2019
4ebf3ad
delete more old files
caspycat Oct 23, 2019
b92017a
add old_name attribute to dashboard vehicles as well
caspycat Oct 23, 2019
220b49b
add Plan object
caspycat Oct 23, 2019
8982284
add BaseDepot and RoutingDepot serializers and deserializers
caspycat Oct 23, 2019
e1c40e5
add is_valid_date helper function to validators modules
caspycat Oct 23, 2019
2652ede
add basic Depot validator (needs extra work)
caspycat Oct 23, 2019
2de4a39
add basic Plan validator (needs extra work)
caspycat Oct 23, 2019
d29c13a
add basic Plan deserializer (needs extra work)
caspycat Oct 23, 2019
4039baf
add __getitem__ and __setitem__ magic methods
caspycat Oct 23, 2019
f1166d3
use plan_id attribute instead of name
caspycat Oct 23, 2019
692c7ad
add PlanRepository
caspycat Oct 23, 2019
a8ad939
fix wrong import
caspycat Oct 23, 2019
70a1c0c
add routing engine api client
caspycat Oct 23, 2019
e6a386d
change repository to use obj instead of name for DELETE method
caspycat Oct 23, 2019
52f0c70
add start_plan and stop_plan methods
caspycat Oct 23, 2019
9537ac3
update readme
caspycat Oct 23, 2019
a77186a
bump version and exclude tests
caspycat Oct 23, 2019
d4e43c1
remove tests/test_module.py
caspycat Oct 23, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 159 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,178 +13,258 @@ You have a fleet of just 10 vehicles to serve 500 spots in the city. Some vehicl

You don't need to. Just throw us a list of stops, vehicles and depots and we will do the heavy lifting for you. _Routing as a Service!_

**BETA RELASE:** ElasticRoute is completely free-to-use until 30th April 2020! Register for an account [here](https://www.elasticroute.com/)
## Preamble

## Quick Start Guide
We offer two API's: The Dashboard API, for developers looking to integrate their existing system with our [ElasticRoute Dashboard](https://www.elasticroute.com/); and the Routing Engine API, for developers looking to solve the Vehicle Routing Problem in a headless environment. The Routing Engine API is only available by request, while the Dashboard API is generally available. Read more [here](https://www.elasticroute.com/routing-engine-api-documentation/).

**Backwards-compatibility notice:** Due to significant overhauls in the backend API, major version 2 of this library is _not_ compatible with code written to work with version 1 of this library.

## Quick Start Guide (Dashboard API)

Install with pip:

pip install elasticroute

In your code, set your default API Key (this can be retrieved from the dashboard of the [web application](https://app.elasticroute.com)):
Create an instance of `DashboardClient`, passing your API key to the constructor. The API Key can be retrieved from the dashboard of the [web application](https://app.elasticroute.com)).

```python
from elasticroute.clients import DashboardClient

dashboard = DashboardClient("YOUR_API_KEY_HERE")
```

You can then programmatically create stops to appear on your Dashboard:

```python
from elasticroute.dashboard import Stop

stop = Stop()
stop["name"] = "Changi Airport"
stop["address"] = "80 Airport Boulevard (S)819642"

dashboard.stops.create(stop)
```

Data attributes of models in this library are accessed and modified using the index operator `[]`. You can get/set any attributes listed in [this page](https://www.elasticroute.com/dashboard-api-documentation/) (under _Field Headers and Description_) that are not marked as **Result** or **Readonly**. Keys passed to the index operator **must** be strings. Passing non-string keys or attempting to modify readonly attributes will trigger a warning.

By default, this creates a stop on today's date. Change the date by passing the `date` keyword argument:

```python
dashboard.stops.create(stop, date="2019-01-01")
```

Date strings must follow the `YYYY-MM-DD` format.

All CRUD operations are available for stops with the following method signatures:

```python
dashboard.stops.create(stop)
dashboard.stops.retrieve(stop_name)
dashboard.stops.update(stop)
dashboard.stops.delete(stop)
```

All methods accept the `date` keyword argument. The `create` method throws an exception (`elasticroute.errors.repository.ERServiceException`) if a stop with an existing name already exists on the same day, while the `retrieve`, `update` and `delete` methods will throw an exception if a stop with the given name does not exist on that day.

CRUD operations are also available for Vehicles:

```python
from elasticroute.dashboard import Vehicle

vehicle = Vehicle()
vehicle["name"] = "Morning shift driver"
vehicle["avail_from"] = 900
vehicle["avail_to"] = 1200

dashboard.vehicles.create(vehicle)
dashboard.vehicles.retrieve(vehicle_name)
dashboard.vehicles.update(vehicle)
dashboard.vehicles.delete(vehicle)
```

Like for stops, the `create` method throws `elasticroute.errors.repository.ERServiceException` if a vehicle with the same name already exists on the same account, while `retrieve`, `update`, `delete` methods will throw an exception if a vehicle with the given name does not yet exist in the account.

Unlike stops, vehicles are not bound by date, and are present across all dates.

Both stops and vehicles accept a dictionary in their constructor that automatically sets their corresponding data attributes.

The library helps you check for invalid values before requests are sent to the server. For instance, setting a vehicle's `avail_to` data attribute to `2500` will trigger a `elasticroute.errors.validator.BadFieldError` when performing any CRUD operations.

Currently, the Dashboard API is unable to perform CRUD operations on depots. Since the details of depots are likely not going to be changed frequently, please configure (using the web application) all the depots that your team has before using this library to perform plans.

### Programmatically starting the planning process

Once you have created more than one stop for the day (and created a starting depot via the web application), you can remotely start and stop the planning process:
```python
import elasticroute as er
er.defaults.API_KEY = "my_super_secret_key"
# where dashboard is an instance of elasticroute.clients.DashboardClient and date is a string in YYYY-MM-DD format
dashboard.stops.start_planning(date)
dashboard.stops.stop_planning(date)
```
## Quick Start Guide (Routing Engine API)

The Routing Engine API is only available by request; please get in touch with us if you require our headless routing capabilities. Attempting to use the Routing Engine API with an unauthorized API Key will result in your requests being rejected.

If you haven't already, install this library:

Create a new `Plan` object and givt it a name/id:
pip install elasticroute>=2.0.0

Create an instance of `RoutingClient`, passing your API key in the constructor:

```python
plan = er.Plan()
plan.id = "my_first_plan"
from elasticroute.clients import RoutingClient

router = RoutingClient("YOUR_API_KEY_HERE")
```

Create a new `Plan` object:

```python
from elasticroute.routing import Plan

plan = Plan("some-unique-id")
```

Give us an array of stops:
Give us a list of stops:

```python
from elasticroute.routing import Stop
plan.stops = [
{
Stop({
"name": "Changi Airport",
"address": "80 Airport Boulevard (S)819642",
},
{
}),
Stop({
"name": "Gardens By the Bay",
"lat": "1.281407",
"lng": "103.865770",
},
}),
# add more stops!
# both human-readable addresses and machine-friendly coordinates work!
]
```

Give us an array of your available vehicles:
Give us a list of your available vehicles:

```python
from elasticroute.routing import Vehicle
plan.vehicles = [
{
Vehicle({
"name": "Van 1"
},
{
}),
Vehicle({
"name": "Van 2"
},
}),
]
```

Give us an array of depots (warehouses):
Give us a list of depots (warehouses):

```python
from elasticroute.routing import Depot
plan.depots = [
{
Depot({
"name": "Main Warehouse",
"address": "61 Kaki Bukit Ave 1 #04-34, Shun Li Ind Park Singapore 417943",
},
}),
]
```

Set your country and timezone (for accurate geocoding):
Set your country and timezone:

```python
plan.generalSettings["country"] = "SG"
plan.generalSettings["timezone"] = "Asia/Singapore"
```

Call `solve()` and save the result to a variable:
Use the client to submit the plan:

```python
solution = plan.solve()
plan = router.plans.create(plan)
```

Inspect the solution!
The planning process is asynchronous as it takes some time to complete. Persist the value of the plan id you used earlier, and retrieve it in a separate process at a later time:

```python
for stop in solution.stops:
print("Stop {} will be served by {} at time {}".format(stop["name"], stop["assign_to"], stop["eta"]))
plan = router.plans.retrieve(plan_id)
```

Quick notes:
`plan.status` should give you `"planned"` when the process is complete. Inspect the solution:

- The individual stops, vehicles and depots can be passed into the `Plan` as either dictionaries or instances of `elasticroute.Stop`, `elasticroute.Vehicle` and `elasticroute.Depot` respectively. Respective properties are the same as the dictionary keys.
- Solving a plan returns you an instance of `elasticroute.Solution`, that has mostly the same properties as `elasticroute.Plan` but not the same functions (see advanced usage)
- Unlike when creating `Plan`'s, `Solution.stops|vehicles|depots` returns you instances of `elasticroute.Stop`, `elasticroute.Vehicle` and `elasticroute.Depot` accordingly instead of dictionaries.
```python
for stop in plan.stops:
print("Stop {} will be served by {} at time {}".format(stop["name"], stop["assign_to"], stop["eta"]))
```

## Advanced Usage

### Setting time constraints

Time constraints for Stops and Vehicles can be set with the `from` and `till` keys of `elasticroute.Stop` and `elasticroute.Vehicle`:
Time constraints for Stops and Vehicles can be set with the `from` and `till` keys of `elasticroute.common.Stop`, and the `avail_from` and `avail_to` keys of `elasticroute.common.Vehicle`:

```python
morning_only_stop = er.Stop()
morning_only_stop = Stop()
morning_only_stop["name"] = "Morning Delivery 1"
morning_only_stop["from"] = 900
morning_only_stop["till"] = 1200
# add address and add to plan...
morning_shift_van = er.Vehicle()
morning_shift_van = Vehicle()
morning_shift_van["name"] = "Morning Shift 1"
morning_shift_van["from"] = 900
morning_shift_van["till"] - 1200
# add to plan and solve...
morning_shift_van["avail_from"] = 900
morning_shift_van["avail_till"] - 1200
# add to plan and solve, or upload to dashboard using DashboardClient
```

Not specifying the `from` and `till` keys of either class would result it being defaulted to `avail_from` and `avail_to` keys in the `elasticroute.defaults.generalSettings` dictionary, which in turn defaults to `500` and `1700`.
`elasticroute.common.Stop` is the parent class of `elasticroute.routing.Stop` and `elasticroute.dashboard.Stop`; Vehicles work in a similar manner

### Setting home depots

A "home depot" can be set for both Stops and Vehicles. A depot for stops indicate where a vehicle must pick up a stop's goods before arriving, and a depot for vehicles indicate the start and end point of a Vehicle's journey (this implicitly assigns the possible jobs a Vehicle can take).
By default, for every stop and vehicle, if the depot field is not specified we will assume it to be the first depot.

```python
common_stop = er.Stop()
common_stop = Stop()
common_stop["name"] = "Normal Delivery 1"
common_stop["depot"] = "Main Warehouse"
# set stop address and add to plan...
rare_stop = er.Stop()
# set stop address
rare_stop = Stop()
rare_stop["name"] = "Uncommon Delivery 1"
rare_stop["depot"] = "Auxillary Warehouse"
# set stop address and add to plan...
plan.vehicles = [
{
"name": "Main Warehouse Van",
"depot": "Main Warehouse"
},
{
"name": "Auxillary Warehouse Van",
"depot": "Auxillary Warehouse"
}
]
# set stop address
main_warehouse_van = Vehicle({
"name": "Main Warehouse Van",
"depot": "Main Warehouse"
})
aux_warehouse_van = Vehicle({
"name": "Auxillary Warehouse Van",
"depot": "Auxillary Warehouse"
})

# if using DashboardClient:
dashboard.stops.create(common_stop)
dashboard.stops.create(rare_stop)
dashboard.vehicles.create(main_warehouse_van)
dashboard.vehicles.create(aux_warehouse_van)

# if using RoutingClient:
plan = Plan("my_plan")
plan.stops = [common_stop, rare_stop]
plan.vehicles = [main_warehouse_van, aux_warehouse_van]
plan.depots = [
{
Depot({
"name": "Main Warehouse",
"address": "Somewhere"
},
{
}),
Depot({
"name": "Auxillary Warehouse",
"address": "Somewhere else"
}
})
]
# solve and get results...
router.plans.create(plan)
```

**IMPORTANT:** The value of the `depot` fields MUST correspond to a matching `elasticroute.Depot` in the same plan with the same name!
For this to work, there must be a corresponding depot with the same name in the dashboard (if using `DashboardClient`) or in the same plan (if using `RoutingClient`)

### Setting load constraints

Each vehicle can be set to have a cumulative maximum weight, volume and (non-cumulative) seating capacity which can be used to determine how many stops it can serve before it has to return to the depot. Conversely, each stop can also be assigned weight, volume and seating loads.
The keys are `weight_load`, `volume_load`, `seating_load` for Stops and `weight_capacity`, `volume_capacity` and `seating_capacity` for Vehicles.

### Alternative connection types (for large datasets)

By default, all requests are made in a _synchronous_ manner. Most small to medium-sized datasets can be solved in less than 10 seconds, but for production uses you probably may one to close the HTTP connection first and poll for updates in the following manner:

```python
import time

plan = er.Plan()
plan.connection_type = "poll";
# do the usual stuff
solution = plan.solve()
while solution.status != "planned":
solution.refresh()
time.sleep(2)
# or do some threading or promise
```

Setting the `connection_type` to `"poll"` will cause the server to return you a response immediately after parsing the request data. You can monitor the status with the `status` and `progress` properties while fetching updates with the `refresh()` method.

In addition, setting the `connectionType` to `"webhook"` will also cause the server to post a copy of the response to your said webhook. The exact location of the webhook can be specified with the `webhook` property of `Plan` objects.
4 changes: 3 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import elasticroute
import os

"""
env_path = Path('tests') / '.env'
load_dotenv(dotenv_path=env_path)

elasticroute.defaults.API_KEY = os.getenv("ELASTICROUTE_API_KEY")
elasticroute.defaults.BASE_URL = os.getenv("ELASTICROUTE_PATH")

print("Default API Key registered as: {}".format(elasticroute.defaults.API_KEY))
print("BASE URL registered as: {}".format(elasticroute.defaults.BASE_URL))
print("BASE URL registered as: {}".format(elasticroute.defaults.BASE_URL))
"""
6 changes: 0 additions & 6 deletions elasticroute/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +0,0 @@
from .exceptions import BadFieldError
from .data_models import Depot, Stop, Vehicle
from . import defaults
from .client_models import Plan, Solution

name = "elasticroute"
Loading