plumber/ 0000755 0001762 0000144 00000000000 13305412327 011720 5 ustar ligges users plumber/inst/ 0000755 0001762 0000144 00000000000 13305335716 012703 5 ustar ligges users plumber/inst/examples/ 0000755 0001762 0000144 00000000000 13304040260 014503 5 ustar ligges users plumber/inst/examples/01-append/ 0000755 0001762 0000144 00000000000 13304040260 016170 5 ustar ligges users plumber/inst/examples/01-append/plumber.R 0000644 0001762 0000144 00000001324 13304040260 017761 0 ustar ligges users values <- 15
MAX_VALS <- 50
#* Append to our values
#* @post /append
function(val, res){
v <- as.numeric(val)
if (is.na(v)){
res$status <- 400
res$body <- "val parameter must be a number"
}
values <<- c(values, val)
if (length(values) > MAX_VALS){
values <<- tail(values, n=MAX_VALS)
}
list(result="success")
}
#* Get the last few values
#* @get /tail
function(n="10", res){
n <- as.numeric(n)
if (is.na(n) || n < 1 || n > MAX_VALS){
res$status <- 400
res$body <- "parameter 'n' must be a number between 1 and 100"
}
list(val=tail(values, n=n))
}
#* Get a graph of the values
#* @png
#* @get /graph
function(){
plot(values, type="b", ylim=c(1,100), main="Recent Values")
}
plumber/inst/examples/02-filters/ 0000755 0001762 0000144 00000000000 13304040260 016372 5 ustar ligges users plumber/inst/examples/02-filters/plumber.R 0000644 0001762 0000144 00000003233 13304040260 020164 0 ustar ligges users library(plumber)
users <- data.frame(
id=1:2,
username=c("joe", "kim"),
groups=c("users", "admin,users")
)
#* Filter that grabs the "username" querystring parameter.
#* You should, of course, use a real auth system, but
#* this shows the principles involved.
#* @filter auth-user
function(req, username=""){
# Since username is a querystring param, we can just
# expect it to be available as a parameter to the
# filter (plumber magic).
# This is a work-around for https://github.com/trestletech/plumber/issues/12
# and shouldn't be necessary long-term
req$user <- NULL
if (username == ""){
# No username provided
} else if (username %in% users$username){
# username is valid
req$user <- users[users$username == username,]
} else {
# username was provided, but invalid
stop("No such username: ", username)
}
# Continue on
forward()
}
#* Now require that all users must be authenticated.
#* @filter require-auth
function(req, res){
if (is.null(req$user)){
# User isn't logged in
res$status <- 401 # Unauthorized
list(error="You must login to access this resource.")
} else {
# user is logged in. Move on...
forward()
}
}
#* @get /me
function(req){
# Safe to assume we have a user, since we've been
# through all the filters and would have gotten an
# error earlier if we weren't.
list(user=req$user)
}
#* Get info about the service. We preempt the
#* require-auth filter because we want authenticated and
#* unauthenticated users alike to be able to access this
#* endpoint.
#* @preempt require-auth
#* @get /about
function(){
list(descript="This is a demo service that uses authentication!")
}
plumber/inst/examples/04-mean-sum/ 0000755 0001762 0000144 00000000000 13304040260 016446 5 ustar ligges users plumber/inst/examples/04-mean-sum/plumber.R 0000644 0001762 0000144 00000000245 13304040260 020240 0 ustar ligges users #* @get /mean
normalMean <- function(samples=10){
data <- rnorm(samples)
mean(data)
}
#* @post /sum
addTwo <- function(a, b){
as.numeric(a) + as.numeric(b)
}
plumber/inst/examples/06-sessions/ 0000755 0001762 0000144 00000000000 13305335716 016612 5 ustar ligges users plumber/inst/examples/06-sessions/plumber.R 0000644 0001762 0000144 00000001511 13304040260 020363 0 ustar ligges users #* @get /counter
function(req, res){
count <- 0
if (!is.null(req$cookies$visitcounter)){
count <- as.numeric(req$cookies$visitcounter)
}
# Most people won't need to concern themselves with the path argument.
# I do because of some peculiarities in how I'm hosting the examples.
res$setCookie("visitcounter", count+1, path="/")
return(paste0("This is visit #", count))
}
#* Example using req$session. Requires adding "sessionCookie()" support to your router in order
#* to work:
#* `pr <- plumb("file.R"); pr$addGlobalProcessor(sessionCookie("secret", "cookieName")); pr$run()`
#* @get /sessionCounter
function(req){
count <- 0
if (!is.null(req$session$counter)){
count <- as.numeric(req$session$counter)
}
req$session$counter <- count + 1
return(paste0("This is visit #", count))
}
#* @assets static
list()
plumber/inst/examples/06-sessions/static/ 0000755 0001762 0000144 00000000000 13304040260 020063 5 ustar ligges users plumber/inst/examples/06-sessions/static/iframe-secure.html 0000644 0001762 0000144 00000003065 13304040260 023504 0 ustar ligges users
Response text
Click "Visit /sessionCounter"
Cookie Value
plumber/inst/examples/06-sessions/static/js-cookie.js 0000644 0001762 0000144 00000006540 13304040260 022311 0 ustar ligges users /*!
* JavaScript Cookie v2.1.0
* https://github.com/js-cookie/js-cookie
*
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
* Released under the MIT license
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
var _OldCookies = window.Cookies;
var api = window.Cookies = factory();
api.noConflict = function () {
window.Cookies = _OldCookies;
return api;
};
}
}(function () {
function extend () {
var i = 0;
var result = {};
for (; i < arguments.length; i++) {
var attributes = arguments[ i ];
for (var key in attributes) {
result[key] = attributes[key];
}
}
return result;
}
function init (converter) {
function api (key, value, attributes) {
var result;
// Write
if (arguments.length > 1) {
attributes = extend({
path: '/'
}, api.defaults, attributes);
if (typeof attributes.expires === 'number') {
var expires = new Date();
expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
attributes.expires = expires;
}
try {
result = JSON.stringify(value);
if (/^[\{\[]/.test(result)) {
value = result;
}
} catch (e) {}
if (!converter.write) {
value = encodeURIComponent(String(value))
.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
} else {
value = converter.write(value, key);
}
key = encodeURIComponent(String(key));
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
key = key.replace(/[\(\)]/g, escape);
return (document.cookie = [
key, '=', value,
attributes.expires && '; expires=' + attributes.expires.toUTCString(), // use expires attribute, max-age is not supported by IE
attributes.path && '; path=' + attributes.path,
attributes.domain && '; domain=' + attributes.domain,
attributes.secure ? '; secure' : ''
].join(''));
}
// Read
if (!key) {
result = {};
}
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all. Also prevents odd result when
// calling "get()"
var cookies = document.cookie ? document.cookie.split('; ') : [];
var rdecode = /(%[0-9A-Z]{2})+/g;
var i = 0;
for (; i < cookies.length; i++) {
var parts = cookies[i].split('=');
var name = parts[0].replace(rdecode, decodeURIComponent);
var cookie = parts.slice(1).join('=');
if (cookie.charAt(0) === '"') {
cookie = cookie.slice(1, -1);
}
try {
cookie = converter.read ?
converter.read(cookie, name) : converter(cookie, name) ||
cookie.replace(rdecode, decodeURIComponent);
if (this.json) {
try {
cookie = JSON.parse(cookie);
} catch (e) {}
}
if (key === name) {
result = cookie;
break;
}
if (!key) {
result[name] = cookie;
}
} catch (e) {}
}
return result;
}
api.get = api.set = api;
api.getJSON = function () {
return api.apply({
json: true
}, [].slice.call(arguments));
};
api.defaults = {};
api.remove = function (key, attributes) {
api(key, '', extend(attributes, {
expires: -1
}));
};
api.withConverter = init;
return api;
}
return init(function () {});
}));
plumber/inst/examples/06-sessions/static/iframe.html 0000644 0001762 0000144 00000003044 13304040260 022215 0 ustar ligges users
HTML here!"
}
#* Download a binary file.
#* @serializer contentType list(type="application/octet-stream")
#* @get /download-binary
function(res){
# TODO: Stream the data into the response rather than loading it all in memory
# first.
# Create a temporary example RDS file
x <- list(a=123, b="hi!")
tmp <- tempfile(fileext=".rds")
saveRDS(x, tmp)
# This header is a convention that instructs browsers to present the response
# as a download named "mydata.Rds" rather than trying to render it inline.
res$setHeader("Content-Disposition", "attachment; filename=mydata.Rds")
# Read in the raw contents of the binary file
bin <- readBin(tmp, "raw", n=file.info(tmp)$size)
# Delete the temp file
file.remove(tmp)
# Return the binary contents
bin
}
plumber/inst/examples/12-entrypoint/ 0000755 0001762 0000144 00000000000 13304040260 017136 5 ustar ligges users plumber/inst/examples/12-entrypoint/myplumberapi.R 0000644 0001762 0000144 00000000324 13304040260 021766 0 ustar ligges users #* @get /counter
function(req){
count <- 0
if (!is.null(req$session$counter)){
count <- as.numeric(req$session$counter)
}
req$session$counter <- count + 1
return(paste0("This is visit #", count))
}
plumber/inst/examples/12-entrypoint/entrypoint.R 0000644 0001762 0000144 00000000140 13304040260 021467 0 ustar ligges users
pr <- plumb("myplumberapi.R")
pr$addGlobalProcessor(sessionCookie("secret", "cookieName"))
pr
plumber/inst/examples/11-car-inventory/ 0000755 0001762 0000144 00000000000 13304040260 017522 5 ustar ligges users plumber/inst/examples/11-car-inventory/plumber.R 0000644 0001762 0000144 00000005701 13304040260 021316 0 ustar ligges users inventory <- read.csv("inventory.csv", stringsAsFactors = FALSE)
#* @apiTitle Auto Inventory Manager
#* @apiDescription Manage the inventory of an automobile
#* store using an API.
#* @apiTag cars Functionality having to do with the management of
#* car inventory.
#* List all cars in the inventory
#* @get /car/
#* @tag cars
listCars <- function(){
inventory
}
#* Lookup a car by ID
#* @param id The ID of the car to get
#* @get /car/
#* @response 404 No car with the given ID was found in the inventory.
#* @tag cars
getCar <- function(id, res){
car <- inventory[inventory$id == id,]
if (nrow(car) == 0){
res$status <- 404
}
car
}
validateCar <- function(make, model, year){
if (missing(make) || nchar(make) == 0){
return("No make specified")
}
if (missing(model) || nchar(model) == 0){
return("No make specified")
}
if (missing(year) || as.integer(year) == 0){
return("No year specified")
}
NULL
}
#* Add a car to the inventory
#* @post /car/
#* @param make:character The make of the car
#* @param model:character The model of the car
#* @param edition:character Edition of the car
#* @param year:int Year the car was made
#* @param miles:int The number of miles the car has
#* @param price:numeric The price of the car in USD
#* @response 400 Invalid user input provided
#* @tag cars
addCar <- function(make, model, edition, year, miles, price, res){
newId <- max(inventory$id) + 1
valid <- validateCar(make, model, year)
if (!is.null(valid)){
res$status <- 400
return(list(errors=paste0("Invalid car: ", valid)))
}
car <- list(
id = newId,
make = make,
model = model,
edition = edition,
year = year,
miles = miles,
price = price
)
inventory <<- rbind(inventory, car)
getCar(newId)
}
#* Update a car in the inventory
#* @param id:int The ID of the car to update
#* @param make:character The make of the car
#* @param model:character The model of the car
#* @param edition:character Edition of the car
#* @param year:int Year the car was made
#* @param miles:int The number of miles the car has
#* @param price:numeric The price of the car in USD
#* @put /car/
#* @tag cars
updateCar <- function(id, make, model, edition, year, miles, price, res){
valid <- validateCar(make, model, year)
if (!is.null(valid)){
res$status <- 400
return(list(errors=paste0("Invalid car: ", valid)))
}
updated <- list(
id = id,
make = make,
model = model,
edition = edition,
year = year,
miles = miles,
price = price
)
if (!(id %in% inventory$id)){
stop("No such ID: ", id)
}
inventory[inventory$id == id, ] <<- updated
getCar(id)
}
#* Delete a car from the inventory
#* @param id:int The ID of the car to delete
#* @delete /car/
#* @tag cars
deleteCar <- function(id, res){
if (!(id %in% inventory$id)){
res$status <- 400
return(list(errors=paste0("No such ID: ", id)))
}
inventory <<- inventory[inventory$id != id,]
}
plumber/inst/examples/11-car-inventory/inventory.csv 0000644 0001762 0000144 00000001126 13304040260 022274 0 ustar ligges users id,make,model,edition,year,miles,price
12049293,Ford,Focus,SE,2016,16827,15000
12049294,GMC,Terrain,SLE-1,2012,23899,15000
12049295,Ford,Escape,SE,2013,48527,15000
12049296,Chevrolet,Silverado,Extended Cab,2000,278806,11500
12049297,Ford,Mustang,GT,1995,73500,9997
12049298,Volvo,S40,2.4i,2007,127733,6000
12049299,Nissan,Maxima,GLE,2003,144000,2450
12049300,Ford,Focus,ST,2014,50776,14991
12049301,Buick,Encore,,2014,77389,14542
12049302,Buick,Regal,,2013,19569,14495
12049303,Ford,Edge,SE,2013,54100,14491
12049304,Toyota,Highlander,,2011,104133,14393
12049305,Toyota,Corolla,LE,2014,25924,13991
plumber/inst/examples/07-mailgun/ 0000755 0001762 0000144 00000000000 13305335716 016401 5 ustar ligges users plumber/inst/examples/07-mailgun/plumber.R 0000644 0001762 0000144 00000000522 13304040260 020153 0 ustar ligges users emails <- data.frame(from=character(0), time=character(0), subject=character(0), stringsAsFactors = FALSE)
#* @post /mail
function(from, subject){
emails <<- rbind(emails, data.frame(from=from, time=date(), subject=htmltools::htmlEscape(subject), stringsAsFactors=FALSE))
TRUE
}
#* @get /tail
function(){
tail(emails[,-1], n=5)
}
plumber/inst/examples/10-welcome/ 0000755 0001762 0000144 00000000000 13304040260 016354 5 ustar ligges users plumber/inst/examples/10-welcome/plumber.R 0000644 0001762 0000144 00000000132 13304040260 020141 0 ustar ligges users #* @get /
#* @html
function(){
"
plumber is alive!
"
}
plumber/inst/examples/03-github/ 0000755 0001762 0000144 00000000000 13305335716 016223 5 ustar ligges users plumber/inst/examples/03-github/plumber.R 0000644 0001762 0000144 00000001477 13304040260 020007 0 ustar ligges users #* Get information about the currently available
#* @get /version
function(){
desc <- read.dcf(system.file("DESCRIPTION", package="plumber"))
resp <- list(
version = unname(desc[1,"Version"]),
built = unname(desc[1,"Built"])
)
if ("GithubSHA1" %in% colnames(desc)){
resp["sha1"] <- unname(desc[1,"GithubSHA1"])
}
resp
}
#* Give GitHub Webhook a way to alert us about new pushes to the repo
#* https://developer.github.com/webhooks/
#* @post /update
function(req, res){
secret <- readLines("./github-key.txt")[1]
hm <- digest::hmac(secret, req$postBody, algo="sha1")
hm <- paste0("sha1=", hm)
if (!identical(hm, req$HTTP_X_HUB_SIGNATURE)){
res$status <- 400
res$body <- "invalid GitHub signature."
return(res)
}
# DO...
devtools::install_github("trestletech/plumber")
TRUE
}
plumber/inst/examples/05-static/ 0000755 0001762 0000144 00000000000 13304040260 016214 5 ustar ligges users plumber/inst/examples/05-static/README.md 0000644 0001762 0000144 00000000515 13304040260 017474 0 ustar ligges users ## Static File Server
This example sets up two static file servers. One at the default path (`/public`), and another at an explicit path (`/static`). You should be able to access the two files in the `./files` directory at either of those paths. So try `http://localhost:8000/static/b.txt` or `http://localhost:8000/public/a.html`.
plumber/inst/examples/05-static/files/ 0000755 0001762 0000144 00000000000 13304040260 017316 5 ustar ligges users plumber/inst/examples/05-static/files/a.html 0000644 0001762 0000144 00000000153 13304040260 020423 0 ustar ligges users
Hello, plumber world
Success!
plumber/inst/examples/05-static/files/b.txt 0000644 0001762 0000144 00000000020 13304040260 020270 0 ustar ligges users text file here!
plumber/inst/examples/05-static/plumber.R 0000644 0001762 0000144 00000000075 13304040260 020007 0 ustar ligges users #* @assets ./files
list()
#* @assets ./files /static
list()
plumber/inst/server/ 0000755 0001762 0000144 00000000000 13304040260 014173 5 ustar ligges users plumber/inst/server/plumber.service 0000644 0001762 0000144 00000000423 13304040260 017222 0 ustar ligges users [Unit]
Description=Plumber API
[Service]
ExecStart=/usr/bin/Rscript -e "pr <- plumber::plumb('/var/plumber$PATH$/plumber.R'); $PREFLIGHT$ pr$run(port=$PORT$, swagger=$SWAGGER$)"
Restart=on-abnormal
WorkingDirectory=/var/plumber/$PATH$/
[Install]
WantedBy=multi-user.target
plumber/inst/server/nginx-ssl.conf 0000644 0001762 0000144 00000000715 13304040260 016767 0 ustar ligges users server {
listen 80 default_server;
listen [::]:80 default_server;
server_name $DOMAIN$;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name $DOMAIN$;
ssl_certificate /etc/letsencrypt/live/$DOMAIN$/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN$/privkey.pem;
include /etc/nginx/sites-available/plumber-apis/*;
location /.well-known/ {
root /var/certbot/;
}
}
plumber/inst/server/nginx.conf 0000644 0001762 0000144 00000000350 13304040260 016163 0 ustar ligges users # Plumber server configuration
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
include /etc/nginx/sites-available/plumber-apis/*;
location /.well-known/ {
root /var/certbot/;
}
}
plumber/inst/server/forward.conf 0000644 0001762 0000144 00000000047 13304040260 016507 0 ustar ligges users location = / {
return 307 /$PATH$;
}
plumber/inst/server/plumber-api.conf 0000644 0001762 0000144 00000000134 13304040260 017255 0 ustar ligges users location /$PATH$/ {
proxy_pass http://localhost:$PORT$/;
proxy_set_header Host $host;
}
plumber/inst/swagger-ui/ 0000755 0001762 0000144 00000000000 13304040260 014737 5 ustar ligges users plumber/inst/swagger-ui/index.html 0000644 0001762 0000144 00000010760 13304040260 016740 0 ustar ligges users
Swagger UI
plumber/inst/hosted-new.R 0000644 0001762 0000144 00000002060 13304040260 015063 0 ustar ligges users library(analogsea)
library(plumber)
install_package_secure <- function(droplet, pkg){
analogsea::install_r_package(droplet, pkg, repo="https://cran.rstudio.com")
}
drop <- plumber::do_provision(unstable=TRUE, example=FALSE, name="hostedplumber")
do_deploy_api(drop, "append", "./inst/examples/01-append/", 8001)
do_deploy_api(drop, "filters", "./inst/examples/02-filters/", 8002)
# GitHub
install_package_secure(drop, "digest")
# devtools is the other dependency, but by unstable=TRUE on do_provision we already have that
do_deploy_api(drop, "github", "./inst/examples/03-github/", 8003)
# Sessions
droplet_ssh(drop, 'R -e "install.packages(\\"PKI\\",,\\"https://www.rforge.net\\")"')
do_deploy_api(drop, "sessions", "./inst/examples/06-sessions/", 8006,
preflight="pr$addGlobalProcessor(plumber::sessionCookie('secret', 'cookieName', path='/'));")
# Mailgun
install_package_secure(drop, "htmltools")
do_deploy_api(drop, "mailgun", "./inst/examples/07-mailgun/", 8007)
# MANUAL: configure DNS, then
# do_configure_https(drop, "plumber.tres.tl"... )
plumber/inst/rstudio/ 0000755 0001762 0000144 00000000000 13304040260 014356 5 ustar ligges users plumber/inst/rstudio/templates/ 0000755 0001762 0000144 00000000000 13304040260 016354 5 ustar ligges users plumber/inst/rstudio/templates/project/ 0000755 0001762 0000144 00000000000 13304040260 020022 5 ustar ligges users plumber/inst/rstudio/templates/project/new-rstudio-project.dcf 0000644 0001762 0000144 00000000212 13304040260 024417 0 ustar ligges users Title: New Plumber API Project
Binding: newRStudioProject
Subtitle: Create a new API using Plumber
Icon: plumber.png
OpenFiles: plumber.R
plumber/inst/rstudio/templates/project/resources/ 0000755 0001762 0000144 00000000000 13304040260 022034 5 ustar ligges users plumber/inst/rstudio/templates/project/resources/plumber.R 0000644 0001762 0000144 00000001364 13304040260 023631 0 ustar ligges users #
# This is a Plumber API. In RStudio 1.2 or newer you can run the API by
# clicking the 'Run API' button above.
#
# In RStudio 1.1 or older, see the Plumber documentation for details
# on running the API.
#
# Find out more about building APIs with Plumber here:
#
# https://www.rplumber.io/
#
library(plumber)
#* @apiTitle Plumber Example API
#* Echo back the input
#* @param msg The message to echo
#* @get /echo
function(msg=""){
list(msg = paste0("The message is: '", msg, "'"))
}
#* Plot a histogram
#* @png
#* @get /plot
function(){
rand <- rnorm(100)
hist(rand)
}
#* Return the sum of two numbers
#* @param a The first number to add
#* @param b The second number to add
#* @post /sum
function(a, b){
as.numeric(a) + as.numeric(b)
}
plumber/inst/rstudio/templates/project/plumber.png 0000644 0001762 0000144 00000021212 13304040260 022174 0 ustar ligges users PNG
IHDR r d ZRd bKGD pHYs tIME:@ IDATxy|Tg=m&컈*k[⾿վ.ZEmݪhߺ[+U@ET@IlI2ϜsIX,Jg 9gs~9cI=?gN`0_3fސ-z'Vf٣ _o+=7WT8`Ѹ3|QQ)OO5Buل`P#JM]@KB P!,S\@Voك(
i y-;d5=4b]U=mA@}khFQ
_Tuk"S
rc/g>Xp$b HeYt:.돧pSVW%BaR!DƗR֖7f)WQ{?4ͯ- S2(Z6}#@^YᦪVK,q0CϼtimI9ӈqg`4VYWYUE>BI#ń&IHHaulrqxw7%bKC\DC
$)=,hv'jRWpH$jm% ŞM?䷷|U՞#-+ڜzIDh]/>f{r:mBARlȲ,{3S:5k/AcHJnMlAFPIosܗA.Tn> pQ$ Ie
!,:+h TcI6$E#WW:`!wyjcѲ}yp`O9$/`uI jzV!7ݩ9q/ 7^ `u>G(1Eu"`H28ds'nTTK)y缫5ՕNEt$ K>s4_EX[>%P^dEŝC躎a#-#
ɀT2oϗB`ۑe'ono 472RNձ.~l?1A_ CHX'˚~|Lfid-#N:.%pZ^}{ %;'
UUӑH]בͫs%Ӱq<2ˑ(e;a
k{"Zw݁l֪8<:!|2-wL&SlSi'?oS^ٌ!9B"I$D1R)IxL9n,}8idQZVBQClHJ:Q,Dyq"2gLDvW
hF/o0o!s<u7mzL1Վj>nl*3e2sִxWoo&?)y'cw9=:rd$6ٛןFO[7~2H%,6TԲr&IOmxs[,\T+/>Ȁ>^0w6]$V5Iױ
: -g)ZƶUd:&8
J`:drԸgȜ5DVƃ=oQn(
㈆1$zef7,1cmUr|GJC=zSRG´K~o|>SIwxfpzpvͅjs2rp.9y̺'\2vSMMdk@8'=@~g2Vl4֕sE"CIàjZa~?{vn/>ά4s~7\Ŗ"b{; Tw+e' Hs_anaYEX)'X8pl:j4y>/|;X<..xkcN.xZeJ\?gF}kgN2sT1=ssrFѲ-}z?<`Z2Gg71ڂ+P^A~S)F
+37l6WS%0h'&ّd[v<wܴR*HOOgҥo-^(`A3+3͟|(KI''+51#Y_ߐ,)sm5L[{mi C pZ]}=ϟσ<ƪ*ꚚiiLsw!K%ZAF/~
~hPlkn;#hM{%?&._1Gڴfתa{0]{Sy?+I{y31}XInw!GdeSR:
.ى677?~-0ݺ{˲1c\RRrALR݇k+l\w,E~\{`.q)δ2Oq=´S08m˖.#6ჹVle5r]:%az_;-{ڹ7pɧA֭