end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

download streaming file by rtmpdump and , convert to mp4 by ffmpeg

#!/usr/local/bin/perl
use strict;
use warnings;
use utf8;
use Encode;
use FindBin;
use LWP::UserAgent;
use XML::Simple;
use Data::Dumper;

## NHKゴガク https://www2.nhk.or.jp/gogaku/english/

my $MUSIC_OUT_DIR = $FindBin::Bin;

my $MUSIC_LIST_ROOT = 'https://www2.nhk.or.jp/gogaku/st/xml';
my $MUSIC_LIST_CHANNELS =
    {english=>[
               'enjoy',         #エンジョイ・シンプル・イングリッシュ
               'basic1',        #基礎英語1
               'basic2',        #基礎英語2
               'basic3',        #基礎英語3
               'kaiwa',         #ラジオ英会話
               'timetrial',     #英会話タイムトライアル
               'kouryaku',      #攻略!英語リスニング
               'business1',     #入門ビジネス英語
               'business2',     #実践ビジネス英語
              ],
    };

# There are some paramaters
# in https://www2.nhk.or.jp/gogaku/st/flash/sound/ggk_str_pc3.swf , below.
# You can extract from swf file
#  by Flare ( http://www.nowrap.de/flare.html ).
my $MUSIC_FILE_ROOT = 'rtmpe://flvs.nhk.or.jp:1935/ondemand';
my $MUSCI_FILE_SUB_PATH = 'mp4:flv/gogaku-stream/mp4';

my $RTMPDUMP_CMD = '/usr/local/bin/rtmpdump';
my $FFMPEG_CMD = '/usr/bin/ffmpeg';

main(@ARGV);

sub main {
    my (@cmd_args) = @_;

    for my $lang (sort keys %$MUSIC_LIST_CHANNELS ){
        for my $channel (@{$MUSIC_LIST_CHANNELS->{$lang}}){
            ## DOWNLOAD CHANNEL INFOS
            my $channel_infos = get_music_list($lang,$channel);
            
            next if(ref($channel_infos) ne 'ARRAY' or
                    scalar(@$channel_infos) ==0 );

            my $out_dir = get_output_dir($lang,$channel);

            for my $channel_info ( @$channel_infos ){
                ## DOWNLOAD MUSIC FILE by rtmpdump
                my $music_file = get_music_file($channel_info, $out_dir);
                next unless $music_file;

                sleep(2);
                
                ## CORRECT by ffmpeg
                my $mp4_file = conv_to_mp4($music_file);
                print Encode::encode('utf8',"DONE $mp4_file"),"\n";
                
                unlink $music_file or die "$music_file$!";
            }
        }
    }
}

sub conv_to_mp4 {
    my($org_file) = @_;

    my $new_file = "$org_file.mp4";
    my $cmd =
        "$FFMPEG_CMD -loglevel error -y -i $org_file -acodec copy $new_file";
    
    my $fh;
    unless( open $fh, '-|', $cmd ){
        print STDERR Encode::encode('utf8',"fail open $cmd"),"\n";
        return;
    }
    unless( close($fh) ){
        print STDERR Encode::encode('utf8',"fail close $cmd"),"\n";
        return;
    }

    return $new_file;
}

sub get_output_dir {
    my($lang,$channel) = @_;

    my $out_dir_0 = join('/',$MUSIC_OUT_DIR,$lang);
    if(not -d $out_dir_0){
        mkdir $out_dir_0 or die "fail mkdir $out_dir_0 $!";
    }
    return $out_dir_0;

    # my $out_dir = join('/',$MUSIC_OUT_DIR,$lang,$channel);
    # if(not -d $out_dir ){
    #     mkdir $out_dir or die "fail mkdir $out_dir $!";
    # }
    # return $out_dir;
}

sub get_music_file {
    my ($channel_info, $out_dir) = @_;

    my $url = join('/',
                   $MUSIC_FILE_ROOT,
                   $MUSCI_FILE_SUB_PATH,
                   $channel_info->{file});
    my $out_file = join('/',
                        $out_dir,
                        "$channel_info->{title}_$channel_info->{hdate}");
    my $cmd = "$RTMPDUMP_CMD --quiet -r $url -o $out_file";
    my $fh;
    unless( open $fh, '-|', $cmd ){
        print STDERR Encode::encode('utf8',"fail open $cmd"),"\n";
        return;
    }
    unless( close($fh) ){
        print STDERR Encode::encode('utf8',"fail close $cmd"),"\n";
        return;
    }

    return $out_file;
}

sub get_music_list {
    my ($lang, $channel) = @_;

    my $url = join('/',$MUSIC_LIST_ROOT,$lang, $channel,'listdataflv.xml');
    my $ua = LWP::UserAgent->new;
    my $res = $ua->get($url);
    if(not $res->is_success ) {
        print STDERR $res->status_line , " $url\n";
        return [];
    }

    my $xml_content = $res->content;
    $xml_content = Encode::decode('utf8',$xml_content);
    my $ret = XML::Simple::XMLin($xml_content);

    return $ret->{music};
}

open amを「 〜.jp」のようなccTLDの場合は3つ以上の「.」が必要

https://github.com/k-tamura/openam-book-jp/blob/master/preparing-for-installation.md に、

テスト目的のためであっても、localhostドメインを使用しないで下さい。 OpenAMの動作は、ドメイン名に基づいて返されるブラウザのクッキーに依存しています。基本的には、少なくとも2つの「.」(ドット)を含むドメイン名を使用していることを確認して下さい。 例) openam.example.com ※正確には、OpenAMインストール時の「Cookie ドメイン」に含める「.」の数は、「〜.com 」のようなgTLDの場合は2つでも構いませんが、「 〜.jp」のようなccTLDの場合は3つ以上が必要です。

しっかり記載されていた。勉強になります。

特に

したがって「.example.com」や「.example.co.jp」は適切であっても、「.example.jp」は不適切ということになります。

には驚いた。

pythonで、任意のdirにライブラリのpathを通す

#!/usr/local/bin/perl
use strict;
use warnings;
use utf8;
use FindBin;
use File::Spec;
use lib File::Spec->catdir($FindBin::Bin, '../lib');
   :

perlでは上記のように「use lib , FindBin」を使用していましたが pythonでは、以下のように「import sys,os」や「sys.path.append()」を使うみたい。

init.py」の配備もポイントみたい

$ cat script/foo.py 
#!/usr/local/bin/python
# coding: utf-8
import sys,os
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../lib')

from TestClass import TestClass

def main():
    test_obj = TestClass(3)
    print test_obj.width


if __name__ == '__main__':
    main()
$ cat lib/TestClass.py
# coding: utf-8

class TestClass(object):
    def __init__(self, width): #constractor
        self.width = width
$ tree
.
├── lib
│   ├── __init__.py
│   └── TestClass.py
└── script
     └── foo.py

App::cpanminus と OrePAN で、localにcpan mirrorを作成し、そこからinstall

internetに接続されていない環境に 対象のperl moduleをinstallする必要があったので、OrePANを使ってみみた。

前準備

事前に本番機には、App::cpanminus (cpanmコマンド)、 開発機?には、OrePAN + App::cpanminus (cpanmコマンド)をinstallしておいて下さい。

開発機にcpan-mirror を作成し、本番機へscp

$ cpanm -L local-tmp --save-dists=cpan-mirror Plack::Middleware::ReverseProxy 
$ cpanm -L local-tmp --save-dists=cpan-mirror Amon2
$ cpanm -L local-tmp --save-dists=cpan-mirror DBI
$ cpanm -L local-tmp --save-dists=cpan-mirror DBD::SQLite
$ cpanm -L local-tmp --save-dists=cpan-mirror DBD::mysql
$ cpanm -L local-tmp --save-dists=cpan-mirror DBIx::Class
  :

↑こちらのようなコマンドで、search.cpan.org からmodule ダウンロードのみ行います。

その後、↓こちらのコマンドでmodule一覧(index file)を作成し、 cpan-mirrorを本番機へscpします。

$ orepan_index.pl -r ./cpan-mirror
$ tar -zcvf cpan-mirror.tar.gz cpan-mirror/
$ scp -i ~/.ssh/id_rsa cpan-mirror.tar.gz ???.???.???.???:tmp/

本番ではscpされたcpan-mirrorからinstall

後は簡単。

# cd /home/endo
# cpanm --mirror=file:///home/endo/cpan-mirror --mirror-only  Amon2
# cpanm --mirror=file:///home/endo/cpan-mirror --mirror-only  DBI
   :

mysql5.7 をsrcからinstall

end0tknr.hateblo.jp

以前、↑こちらで mysql5.5をinstallしましたが、5.7では少々異なりましたので

install mysql 5.7

$ wget http://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.14.tar.gz
$ tar -xvf mysql-5.7.14.tar.gz
$ cd mysql-5.7.14
$ cmake . \
   -DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
   -DDEFAULT_CHARSET=utf8 \
   -DDEFAULT_COLLATION=utf8_general_ci \
   -DENABLED_LOCAL_INFILE=true \
   -DWITH_INNOBASE_STORAGE_ENGINE=1 \
   -DWITH_EXTRA_CHARSETS=all \
   -DWITH_READLINE=ON
  :
CMake Error at cmake/boost.cmake:81 (MESSAGE):
  You can download it with -DDOWNLOAD_BOOST=1 -DWITH_BOOST=<directory>

  This CMake script will look for boost in <directory>.  If it is not there,
  it will download and unpack it (in that directory) for you.

  If you are inside a firewall, you may need to use an http proxy:

  export http_proxy=http://example.com:80

Call Stack (most recent call first):
  cmake/boost.cmake:238 (COULD_NOT_FIND_BOOST)
  CMakeLists.txt:455 (INCLUDE)
-- Configuring incomplete, errors occurred!

mysql5.7よりboostが必要になったようです。

そこで、cmakeのoptionに 「-DDOWNLOAD_BOOST=1 -DWITH_BOOST=/home/endo/tmp/」を追加すると boostを勝手にdownloadしてくれます。

cmake . \
   -DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
   -DDEFAULT_CHARSET=utf8 \
   -DDEFAULT_COLLATION=utf8_general_ci \
   -DENABLED_LOCAL_INFILE=true \
   -DWITH_INNOBASE_STORAGE_ENGINE=1 \
   -DWITH_EXTRA_CHARSETS=all \
   -DWITH_READLINE=ON \
   -DDOWNLOAD_BOOST=1 \
   -DWITH_BOOST=/home/endo/tmp/

$ make
$ make test
# make install

edit /etc/my.conf

ポイントは - skip-grant-tables がないと、id/pw=root/null でログインできません - sql-mode="ONLY_FULL_GROUP_BY" となっていると、selectする全colをgroup by で指定する必要があります

# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html
# *** DO NOT EDIT THIS FILE. It's a template which will be copied to the
# *** default location during install, and will be replaced if you
# *** upgrade to a newer version of MySQL.

[mysqld]

# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M

# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin

# These are commonly set, remove the # and set as required.
# basedir = .....
# datadir = .....
# port = .....
# server_id = .....
# socket = .....
default-storage-engine = INNODB
basedir = /usr/local/mysql
datadir = /home/mysql/data
port            = 3306
socket          = /tmp/mysql.sock

skip-grant-tables
#sql-mode="ONLY_FULL_GROUP_BY"
sql-mode=""

# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M 

# sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES 


# max connections
max_connections = 64
# table_open_cache = (max_connections * tables used in one transaction) + alpha
table_open_cache = 800
# table_definition_cache = (all tables in mysql + max_connections) + alpha
table_definition_cache = 400
# open_files_limit = table_open_cache * 1.4
open_files_limit = 1120
 
# global buffer
key_buffer_size = 16M
query_cache_type = 0
# innodb_buffer_pool_size = RAM for Mysql * 0.7
innodb_buffer_pool_size = 256M
 
# thread buffer
read_buffer_size = 256K
read_rnd_buffer_size = 512K
join_buffer_size = 256K
sort_buffer_size = 512K
 
# InnoDB
innodb_file_per_table
innodb_autoextend_increment = 64
innodb_log_files_in_group = 2
innodb_log_file_size = 64M
innodb_log_buffer_size = 16M
innodb_flush_log_at_trx_commit = 1
innodb_flush_neighbors=0
#innodb_flush_method=O_DIRECT
innodb_thread_concurrency = 4
innodb_commit_concurrency = 4
 
# log
general_log = 0
general_log_file = /usr/local/var/mysql/general.log
slow_query_log = 0
 
[mysqldump]
#max_allowed_packet = 16M
quick
set-charset
single-transaction

install apache2.2 + perl5.18 + mod_perl2.0.9

install perl 5.18

$ wget http://www.cpan.org/src/5.0/perl-5.18.4.tar.gz
$ tar -xvf perl-5.18.4.tar.gz
$ cd perl-5.18.4
$ ./Configure -Dusethreads -Accflags="-fPIC" -de
$ make
$ make test
$ su
# make install

※「-Dusethreads -Accflags="-fPIC"」option are needed by mod_perl.

install apache2.2

$ wget http://ftp.riken.jp/net/apache//httpd/httpd-2.2.31.tar.gz
$ tar -xvf httpd-2.2.31.tar.gz
$ cd httpd-2.2.31
$ ./configure --prefix=/home/endo/local/apache22 \
              --with-mpm=prefork \
              --enable-proxy \
              --enable-modules=all \
              --enable-so
$ make
$ make install

install mod_perl

$ wget http://www.apache.org/dyn/closer.cgi/perl/mod_perl-2.0.9.tar.gz
$ tar -xvf mod_perl-2.0.9.tar.gz
$ cd mod_perl-2.0.9
$ /usr/local/bin/perl Makefile.PL \
          USE_APXS=1 \
          WITH_APXS=/home/endo/local/apache22/bin/apxs \
          EVERYTHING=1
$ make
$ make test
$ su
# make install

edit httpd.conf & startup.pl

$ vi /home/endo/local/apache22/conf/httpd.conf
  :
LoadModule perl_module modules/mod_perl.so
PerlRequire /home/endo/local/apache22/conf/startup.pl

PerlSetEnv XING_CONF     /home/endo/dev/xing/etc/config.yaml

<Directory "/home/endo/dev/xing/app">
    AllowOverride All
    Order allow,deny
    Allow from all

    AuthType BASIC
    AuthUserFile /home/endo/dev/htpasswd
    AuthName "COLINUX MEMBERS"
    require valid-user

    <Files "*.pl">
       Options ExecCGI
       AddHandler cgi-script .pl
       SetHandler perl-script
       PerlHandler ModPerl::Registry
       PerlSendHeader On
    </Files>
</Directory>
Alias /Xing /home/endo/dev/xing/app
$ vi /home/endo/local/apache22/conf/startup.pl
  :
#/usr/local/bin/perl

BEGIN {
    use lib qw(.
               /home/endo/dev/xing/lib
             );
}

use NMW::Template;
$NMW::Template::ENCODING = 'utf8';
@NMW::Template::template_path=('/home/endo/dev/xing/tmpl');

use CGI;
$CGI::LIST_CONTEXT_WARN = 0;

#use Devel::Cover;
#$DEVEL_COVER_OPTIONS='-dir,/home/endo/tmp';

1;

nginx + nginx-auth-ldap module

step1/3 - install

$ cd /home/endo/tmp

$ wget http://nginx.org/download/nginx-1.9.3.tar.gz
$ tar -xvf nginx-1.9.3.tar.gz

$ wget https://www.openssl.org/source/openssl-1.0.2d.tar.gz
$ tar -xvf openssl-1.0.2d.tar.gz

$ git clone https://github.com/kvspb/nginx-auth-ldap.git

$ cd nginx-1.9.3
$ ./configure --prefix=/home/endo/local/nginx \
              --with-http_ssl_module \
              --with-openssl=../openssl-1.0.2d \
              --add-module=../nginx-auth-ldap
$ make
$ make install

step2/3 - edit nginx.conf

$ vi /home/endo/local/nginx/conf/nginx_auth_ldap.conf

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    ldap_server ldap_sexy {
        url ldap://ldap.sexy.co.jp/ou=people,o=sexy-group?uid?sub?(objectClass=*)
        group_attribute uniqueMember;
        group_attribute_is_dn on;
        require valid_user;
    }

    server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            auth_ldap "AUTH_LDAP";
            auth_ldap_servers ldap_sexy;
            root   html;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

step 3/3 - start nginx

$ cd /home/endo/local/nginx
$ ./sbin/nginx -c conf/nginx_auth_ldap.conf

VirtualBoxのストレージをvdmk(可変サイズ)→vdi形式(固定サイズ)に変更

vdi形式(固定サイズ)の方が、ディスクI/Oが良い気がするので、次のような手順でやってみた。

STEP1 ファイル → 仮想メディアマネージャ

f:id:end0tknr:20160818103848p:plain

STEP2 イメージファイルのコピー

※VDI(固定サイズ)、VDMK(可変サイズ) f:id:end0tknr:20160818103854p:plain f:id:end0tknr:20160818103857p:plain

STEP3 旧ストレージをun-mount、新ストレージをmount

f:id:end0tknr:20160818103904p:plain

以上!!

word2013にプログラムコードを貼るなら、セクション区切り + 行番号表示 + ページ罫線 + 行間隔調整

で、ソースがそれらしく見えます。

step1 ページレイアウト → セクション区切り + 行番号表示

f:id:end0tknr:20160810133916p:plain

step2 デザイン → ページ罫線

f:id:end0tknr:20160810133914p:plain

step3 ホーム → 行間隔調整

f:id:end0tknr:20160810133918p:plain

フォントの調整はお好みで

mysqlのLOAD DATA LOCAL INFILEによる bulk insert オレオレまとめ

sqlの"--"コメントにも記載していますが、自分としてのポイントは、

  • LOCAL INFILEの"LOCAL"で、"Access denied for user..."のようなエラーになりづらい
  • "\N"としなくても、NULLIF()でnull値をimportできます

かな?

LOAD DATA LOCAL INFILE
'/path/to/import_file.csv'
[REPLACE | IGNORE]
INTO TABLE import_tbl
character set 'utf8'              -- 文字コード
FIELDS TERMINATED BY ','
       OPTIONALLY ENCLOSED BY '"' -- フィールドを囲む文字
       ESCAPED BY '\'             -- エスケープ文字
LINES  TERMINATED BY '\n'
IGNORE 0 LINES                    -- 先頭?行を無視
(field_1,field_2,...)             -- import対象のcolumn
SET                               -- 必要に応じ値を変換
 update_time = NULLIF(update_time, '0000-00-00 00:00:00'), --日時の場合は注意
 field_1     = IF(field_1='A', field_1, null),
;

まぁ、dev.mysql.com/doc に記載の通りなんですけどね。 https://dev.mysql.com/doc/refman/5.6/ja/load-data.html

(再)Amon2 for perl のControllerに対するTest::Moreによるunit test

end0tknr.hateblo.jp

↑こんな風にplug-inを書いて頑張らなくても Plack::Util::load_psgi や Test::WWW::Mechanize::PSGI で十分実現できるそうです。↓

use strict;
use warnings;
use utf8;
use HTTP::Cookies;
use JSON;
use Plack::Test;
use Plack::Util;
use Test::More;
use Test::Requires 'Test::WWW::Mechanize::PSGI';
use Data::Dumper;

$ENV{"PLACK_ENV"} = 'development';

my $cookie_jar = HTTP::Cookies->new(file => "./cookie.txt",
                                    autosave => 1,
                                    ignore_discard => 1);
$cookie_jar->set_cookie(undef,                  #version
                        "HTTP_mavi-id",         #key
                        'ushiro',               #val
                        "/",                    #pathu
                        "localhost.local");     #domain

my $app = Plack::Util::load_psgi 'script/ean-server';
my $mech = Test::WWW::Mechanize::PSGI->new(app => $app,
                                           cookie_jar=>$cookie_jar);

subtest 'sub_test_name_1'=> sub {
    $mech->get_ok("/");
    $mech->title_like(qr/トップ/s);
    $mech->content_like(qr/ean/s);
};

subtest 'sub_test_name_2'=> sub {
    $mech->get_ok("/report/group/settings");
    my $res_data = JSON::from_json( $mech->content );

    print STDERR Dumper($res_data);
    
};

done_testing;

Amon2 for perl のControllerに対するTest::Moreによるunit test

以下の get_dummy_context() 内に記載しているようにAmon2クラスに対して、plug-inを書くとOK

use strict;
use utf8;
use t::Util;
use CGI;
use Date::Calc;
use JSON;
use Plack::Session;
use Plack::Test;
use Plack::Util;
use Test::More;
use Data::Dumper;

Ean->bootstrap;
use_ok('Ean::Controller::MemberEdit');
use_ok('Ean::ObjModel::Member');

my $SMART_ID = 'mae';
my $CGI;
my $SESSION;
my $REQUEST;


subtest 'confirm' => sub {

    my $class = 'Ean::Controller::MemberEdit';

    my $c = get_dummy_context();
    $c->session->set( navi_id => $SMART_ID );

    $c->req->param( 'navi_email_send', 1 );
    $c->req->param( 'ean_email1',      'ndds-test@example.com' );
    $c->req->param( 'ean_email1_conf', 'ndds-test@example.com' );
    $c->req->param( 'ean_email1_send', 1 );
    my $ret_data = $class->confirm($c);
    #JSONで返す型(int)もcheck
    like($ret_data, qr/"ean_email2_send":1/o);
    like($ret_data, qr/"show_bems_area_1":1/o);
    like($ret_data, qr/"show_bems_area_2":1/o);
    like($ret_data, qr/"navi_email_send":1/o);
    like($ret_data, qr/"ean_email1":"ndds-test\@example.com"/o);
    like($ret_data, qr/"ean_email1_conf":"ndds-test\@example.com"/o);
    like($ret_data, qr/"ean_email1_send":1/o);
    like($ret_data, qr/"ean_email2":"ndds-test2\@example.com"/o);
    like($ret_data, qr/"ean_email2_conf":"ndds-test2\@example.com"/o);

};


sub date_str {
    my ($day_diff) = @_;
    my @date = Date::Calc::Add_Delta_Days(Date::Calc::Today, $day_diff||0);
    return sprintf("%04d-%02d-%02d", @date);
}

sub now_str {
    my ($day_diff) = @_;
    my @datetime =
        Date::Calc::Add_Delta_YMDHMS(Date::Calc::Today_and_Now,
                                     0,0,$day_diff||0, 0,0,0);
    return sprintf("%04d-%02d-%02d %02d:%02d:%02d", @datetime);
}


sub get_dummy_context {
    no strict 'refs';
    no warnings 'redefine';

    *{"Ean\::session"} =
        sub {
            return $SESSION if $SESSION;

            my $dummy_env = {'psgix.session'=>{},
                             'psgix.session.options'=>{}};
            $SESSION = Plack::Session->new( $dummy_env );
            return $SESSION;
        };
    *{"Ean\::req"} =
        sub {
            return $CGI if $CGI;

            $CGI = CGI->new();
            return $CGI;
        };

    *{"Ean\::render_json"} =
        sub {
            my ($self,$perl_obj) = @_;
            return JSON::to_json($perl_obj);
        };

    *{"Ean\::request"} =
        sub {
            return $REQUEST if $REQUEST;

            $REQUEST = DummyRequest->new();
            return $REQUEST;
        };
    
    my $c = Ean->bootstrap;
    return $c;
}


done_testing;


package DummyRequest;

sub new {
    my ($class) = @_;
    my $self = {};
    $self =  bless $self, $class;
    return $self;
}

sub cookies { return {}; }
sub env { return {}; }

__DATA__