<commit_before># -*- coding: utf-8 -*-
# Python 2.7 support.
from __future__ import division
from enum import Enum

from collections import namedtuple

from .techtree import Node as ResearchNode

# Source: http://wiki.kerbalspaceprogram.com/wiki/Parts, as well as in-game info

kspversion = '1.1.3'

class RadialSize(Enum):
    Tiny = 1
    Small = 2
    Large = 3
    ExtraLarge = 4
    RadiallyMounted = 9      # radially mounted

# A note about length of engine: We have this option, because for landers with landing legs, short
# engines are an advantage. So which height an engine has, depends on the compatibility with landing
# struts, ordered by their mass. We have this data from KSP wiki. Unfortunately, the listings there
# are not comprehensive.
#  0 -> ok with LT-05 µ landing strut (all radially mounted, Terrier, Spark, Ant)
#  1 -> ok with LT-1 landing struts (Dart)
#  2 -> ok with LT-2 landing struts (Poodle, Reliant, Swivel)
#  3 -> all others...

LiquidFuelEngine = namedtuple('LiquidFuelEngine', ['size', 'name', 'cost', 'm', 'isp_atm',
    'isp_vac', 'F_vac', 'tvc', 'level', 'electricity', 'length'])

LiquidFuelEngines = [
        LiquidFuelEngine(RadialSize.RadiallyMounted, 'LV-1R Spider', 120, 20, 260, 290, 2000, 8,
            ResearchNode.PrecisionPropulsion, 0, 0),
        LiquidFuelEngine(RadialSize.RadiallyMounted, '24-77 Twitch', 400, 90, 250, 290, 16000, 8,
            ResearchNode.PrecisionPropulsion, 0, 0),
        LiquidFuelEngine(RadialSize.RadiallyMounted, 'Mk-55 Thud', 820, 900, 275, 305, 120000, 8,
            ResearchNode.AdvancedRocketry, 0, 0),
        LiquidFuelEngine(RadialSize.Tiny,  'LV-1 Ant',       110,  20,   80,  315, 2000,   0,
            ResearchNode.PropulsionSystems, 0, 0),
        LiquidFuelEngine(RadialSize.Tiny,  '48-7S Spark',    200,  100,  270, 300, 18000,  3,
            ResearchNode.PropulsionSystems, 0, 0),
        LiquidFuelEngine(RadialSize.Small, 'LV-909 Terrier', 390,  500,  85,  345, 60000,  4,
            ResearchNode.AdvancedRocketry, 0, 0),
        LiquidFuelEngine(RadialSize.Small, 'LV-T30 Reliant', 1100, 1250, 280, 300, 215000, 0,
            ResearchNode.GeneralRocketry, 1, 2),
        LiquidFuelEngine(RadialSize.Small, 'LV-T45 Swivel',  1200, 1500, 270, 320, 200000, 3,
            ResearchNode.BasicRocketry, 1, 2),
        LiquidFuelEngine(RadialSize.Small, 'S3 KS-25 Vector',18000,4000, 295, 315, 1000000, 10.5,
            ResearchNode.VeryHeavyRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.Small, 'CR7 RAPIER',     6000, 2000, 275, 305, 180000, 3,
            ResearchNode.AerospaceTech, 0, 3),
        LiquidFuelEngine(RadialSize.Small, 'T-1 Dart',       3850, 1000, 290, 340, 180000, 0,
            ResearchNode.HypersonicFlight, 1, 1),
        LiquidFuelEngine(RadialSize.Large, 'RE-L10 Poodle',  1300, 1750, 90,  350, 250000, 4.5,
            ResearchNode.HeavyRocketry, 1, 2),
        LiquidFuelEngine(RadialSize.Large, 'RE-I5 Skipper',  5300, 3000, 280, 320, 650000, 2,
            ResearchNode.HeavyRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.Large, 'RE-M3 Mainsail', 13000,6000, 285, 310, 1500000,2,
            ResearchNode.HeavierRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.Large, 'LFB Twin-Boar',  11250,6500, 280, 300, 2000000, 1.5,
            ResearchNode.HeavierRocketry, 0, 3),
        LiquidFuelEngine(RadialSize.ExtraLarge, 'KR-2L+ Rhino', 25000,9000, 255, 340, 2000000, 4,
            ResearchNode.VeryHeavyRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.ExtraLarge, 'KS-25x4 Mammoth', 39000,15000, 295, 315, 4000000, 2,
            ResearchNode.VeryHeavyRocketry, 1, 3) ]

# Twin-Boar is the engine as listed above, with forced addition of the 36 ton large liquid fuel
# tank (TwinBoarPseudoTank)

# liquid fuel tank quantities.
# empty weight is 1/9 of full weight.
# content is 0.9 parts liquid fuel and 1.1 parts oxidizer.

FuelTank = namedtuple('FuelTank', ['name', 'size', 'cost', 'm_full'])

RocketFuelTanks = [
        FuelTank('Oscar-B', RadialSize.Tiny, 70, 225),               # 0
        FuelTank('FL-T100', RadialSize.Small, 150, 562.5),           # 1
        FuelTank('FL-T200', RadialSize.Small, 275, 1125),
        FuelTank('FL-T400', RadialSize.Small, 500, 2250),
        FuelTank('FL-T800', RadialSize.Small, 800, 4500),            # 4
        FuelTank('X200-8',  RadialSize.Large, 800, 4500),            # 5
        FuelTank('X200-16', RadialSize.Large, 1550, 9000),
        FuelTank('X200-32', RadialSize.Large, 3000, 18000),
        FuelTank('Jumbo-64',RadialSize.Large, 5750, 36000),          # 8
        FuelTank('S3-3600', RadialSize.ExtraLarge, 3250, 20250),     # 9
        FuelTank('S3-7200', RadialSize.ExtraLarge, 6500, 40500),
        FuelTank('S3-14400',RadialSize.ExtraLarge, 13000, 81000) ]   # 11

SmallestTank = { RadialSize.Tiny : 0, RadialSize.Small: 1, RadialSize.Large: 5, RadialSize.ExtraLarge: 9 }
BiggestTank =  { RadialSize.Tiny : 0, RadialSize.Small: 4, RadialSize.Large: 8, RadialSize.ExtraLarge: 11 }

TwinBoarPseudoTank = FuelTank('LFB Twin-Boar', RadialSize.Large, 5750, 36000)

class FuelTypes(Enum):
    LiquidFuel = ('Liquid fuel (+Oxidizer)', 5)
    AtomicFuel = ('Atomic fuel', 5)    # atomic fuel is liquid fuel w/out oxidizer
    Xenon = ('Xenon', 0.1)
    Monopropellant = ('Monopropellant', 4)
    def __init__(self, pname, unitmass):
        self.pname = pname
        self.unitmass = unitmass

SpecialEngine = namedtuple('SpecialEngine', ['size', 'name', 'cost', 'm', 'isp_atm', 'isp_vac',
    'F_vac', 'tvc', 'level', 'electricity', 'length'])
SpecialFuelTank = namedtuple('SpecialFuelTank', ['name', 'size', 'cost', 'm_full', 'f_e', 'level'])

AtomicRocketMotor = SpecialEngine(RadialSize.Small, 'LV-N Nerv Atomic Rocket Motor', 10000, 3000,
        185, 800, 60000, 0, ResearchNode.NuclearPropulsion, 1, 3)
AtomicTankFactor = 23/45    # Mass of full atomic fuel tank / Mass of full liquid fuel tank
AtomicTank_f_e = 5/18

ElectricPropulsionSystem = SpecialEngine(RadialSize.Tiny, 'IX-6315 Dawn Electric Propulsion System',
        8000, 250, 100, 4200, 2000, 0, ResearchNode.IonPropulsion, 0, 0)
XenonTanks = [
    SpecialFuelTank('PB-X150', RadialSize.Tiny, 3600, 125, 11/14, ResearchNode.IonPropulsion),
    SpecialFuelTank('PB-X750', RadialSize.Small, 22500, 940, 41/53, ResearchNode.IonPropulsion),
    SpecialFuelTank('PB-X50R', RadialSize.RadiallyMounted, 2200, 70, 3 / 4, ResearchNode.IonPropulsion) ]

MonoPropellantEngine = \
        SpecialEngine(RadialSize.RadiallyMounted, 'O-10 Puff MonoPropellant Fuel Engine',
                      150, 90, 120, 250, 20000, 0, ResearchNode.PrecisionPropulsion, 0, 0)
MonoPropellantTanks = [
        SpecialFuelTank('FL-R10', RadialSize.Tiny,  200,  370, 5/32, ResearchNode.AdvancedFuelSystems),
        SpecialFuelTank('FL-R25', RadialSize.Small, 600,  1150, 3/20, ResearchNode.AdvancedFuelSystems),
        SpecialFuelTank('FL-R1',  RadialSize.Large, 1300, 3400, 2/15, ResearchNode.AdvancedFuelSystems),
        SpecialFuelTank('Stratus-V Roundified', RadialSize.RadiallyMounted, 200, 315, 5 / 16,
                        ResearchNode.AdvancedFlightControl),
        SpecialFuelTank('Stratus-V Cylindrified', RadialSize.RadiallyMounted, 450, 750, 1 / 4,
                        ResearchNode.SpecializedControl) ]

SolidFuelBooster = namedtuple('SolidFuelBooster', ['name', 'cost', 'm_full', 'm_empty', 'isp_atm', 'isp_vac', 'F_vac', 'level'])

SolidFuelBoosters = [
        SolidFuelBooster('RT-5 Flea',    200,  1500,  450,  140, 165, 192000, ResearchNode.Start),
        SolidFuelBooster('RT-10 Hammer', 400,  3560,  750,  170, 195, 227000, ResearchNode.BasicRocketry),
        SolidFuelBooster('BACC Thumper', 850,  7650,  1500, 175, 210, 300000, ResearchNode.GeneralRocketry),
        SolidFuelBooster('S1 Kickback',  2700, 24000, 4500, 195, 220, 670000, ResearchNode.HeavyRocketry) ]

# Extra for stacked stage
StackstageExtraMass = 50
StackstageExtraCost = 400
StackstageExtraNote = "TR-18A Stack Decoupler"
StackstageExtraTech = ResearchNode.Engineering101

# Extra for radial stage
# This is a very heavy but safe default setting, also having the effect of
# reducing the count of extra radial solid fuel boosters.
RadialstageExtraMass = 50 + 75 + 2*50
RadialstageExtraCost = 700 + 320 + 2*42
RadialstageExtraNote = "TT-70 Radial Decoupler, Advanced Nose Cone, 2 * EAS-4 Strut Connector"
RadialstageExtraTech = ResearchNode.Stability   # actually TT-70 is not there, but with TT-38K the
                                                # first radial decoupler.
<commit_msg>Fix Xenon tank parameters in parts.py<commit_after># -*- coding: utf-8 -*-
# Python 2.7 support.
from __future__ import division
from enum import Enum

from collections import namedtuple

from .techtree import Node as ResearchNode

# Source: http://wiki.kerbalspaceprogram.com/wiki/Parts, as well as in-game info

kspversion = '1.1.3'

class RadialSize(Enum):
    Tiny = 1
    Small = 2
    Large = 3
    ExtraLarge = 4
    RadiallyMounted = 9      # radially mounted

# A note about length of engine: We have this option, because for landers with landing legs, short
# engines are an advantage. So which height an engine has, depends on the compatibility with landing
# struts, ordered by their mass. We have this data from KSP wiki. Unfortunately, the listings there
# are not comprehensive.
#  0 -> ok with LT-05 µ landing strut (all radially mounted, Terrier, Spark, Ant)
#  1 -> ok with LT-1 landing struts (Dart)
#  2 -> ok with LT-2 landing struts (Poodle, Reliant, Swivel)
#  3 -> all others...

LiquidFuelEngine = namedtuple('LiquidFuelEngine', ['size', 'name', 'cost', 'm', 'isp_atm',
    'isp_vac', 'F_vac', 'tvc', 'level', 'electricity', 'length'])

LiquidFuelEngines = [
        LiquidFuelEngine(RadialSize.RadiallyMounted, 'LV-1R Spider', 120, 20, 260, 290, 2000, 8,
            ResearchNode.PrecisionPropulsion, 0, 0),
        LiquidFuelEngine(RadialSize.RadiallyMounted, '24-77 Twitch', 400, 90, 250, 290, 16000, 8,
            ResearchNode.PrecisionPropulsion, 0, 0),
        LiquidFuelEngine(RadialSize.RadiallyMounted, 'Mk-55 Thud', 820, 900, 275, 305, 120000, 8,
            ResearchNode.AdvancedRocketry, 0, 0),
        LiquidFuelEngine(RadialSize.Tiny,  'LV-1 Ant',       110,  20,   80,  315, 2000,   0,
            ResearchNode.PropulsionSystems, 0, 0),
        LiquidFuelEngine(RadialSize.Tiny,  '48-7S Spark',    200,  100,  270, 300, 18000,  3,
            ResearchNode.PropulsionSystems, 0, 0),
        LiquidFuelEngine(RadialSize.Small, 'LV-909 Terrier', 390,  500,  85,  345, 60000,  4,
            ResearchNode.AdvancedRocketry, 0, 0),
        LiquidFuelEngine(RadialSize.Small, 'LV-T30 Reliant', 1100, 1250, 280, 300, 215000, 0,
            ResearchNode.GeneralRocketry, 1, 2),
        LiquidFuelEngine(RadialSize.Small, 'LV-T45 Swivel',  1200, 1500, 270, 320, 200000, 3,
            ResearchNode.BasicRocketry, 1, 2),
        LiquidFuelEngine(RadialSize.Small, 'S3 KS-25 Vector',18000,4000, 295, 315, 1000000, 10.5,
            ResearchNode.VeryHeavyRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.Small, 'CR7 RAPIER',     6000, 2000, 275, 305, 180000, 3,
            ResearchNode.AerospaceTech, 0, 3),
        LiquidFuelEngine(RadialSize.Small, 'T-1 Dart',       3850, 1000, 290, 340, 180000, 0,
            ResearchNode.HypersonicFlight, 1, 1),
        LiquidFuelEngine(RadialSize.Large, 'RE-L10 Poodle',  1300, 1750, 90,  350, 250000, 4.5,
            ResearchNode.HeavyRocketry, 1, 2),
        LiquidFuelEngine(RadialSize.Large, 'RE-I5 Skipper',  5300, 3000, 280, 320, 650000, 2,
            ResearchNode.HeavyRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.Large, 'RE-M3 Mainsail', 13000,6000, 285, 310, 1500000,2,
            ResearchNode.HeavierRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.Large, 'LFB Twin-Boar',  11250,6500, 280, 300, 2000000, 1.5,
            ResearchNode.HeavierRocketry, 0, 3),
        LiquidFuelEngine(RadialSize.ExtraLarge, 'KR-2L+ Rhino', 25000,9000, 255, 340, 2000000, 4,
            ResearchNode.VeryHeavyRocketry, 1, 3),
        LiquidFuelEngine(RadialSize.ExtraLarge, 'KS-25x4 Mammoth', 39000,15000, 295, 315, 4000000, 2,
            ResearchNode.VeryHeavyRocketry, 1, 3) ]

# Twin-Boar is the engine as listed above, with forced addition of the 36 ton large liquid fuel
# tank (TwinBoarPseudoTank)

# liquid fuel tank quantities.
# empty weight is 1/9 of full weight.
# content is 0.9 parts liquid fuel and 1.1 parts oxidizer.

FuelTank = namedtuple('FuelTank', ['name', 'size', 'cost', 'm_full'])

RocketFuelTanks = [
        FuelTank('Oscar-B', RadialSize.Tiny, 70, 225),               # 0
        FuelTank('FL-T100', RadialSize.Small, 150, 562.5),           # 1
        FuelTank('FL-T200', RadialSize.Small, 275, 1125),
        FuelTank('FL-T400', RadialSize.Small, 500, 2250),
        FuelTank('FL-T800', RadialSize.Small, 800, 4500),            # 4
        FuelTank('X200-8',  RadialSize.Large, 800, 4500),            # 5
        FuelTank('X200-16', RadialSize.Large, 1550, 9000),
        FuelTank('X200-32', RadialSize.Large, 3000, 18000),
        FuelTank('Jumbo-64',RadialSize.Large, 5750, 36000),          # 8
        FuelTank('S3-3600', RadialSize.ExtraLarge, 3250, 20250),     # 9
        FuelTank('S3-7200', RadialSize.ExtraLarge, 6500, 40500),
        FuelTank('S3-14400',RadialSize.ExtraLarge, 13000, 81000) ]   # 11

SmallestTank = { RadialSize.Tiny : 0, RadialSize.Small: 1, RadialSize.Large: 5, RadialSize.ExtraLarge: 9 }
BiggestTank =  { RadialSize.Tiny : 0, RadialSize.Small: 4, RadialSize.Large: 8, RadialSize.ExtraLarge: 11 }

TwinBoarPseudoTank = FuelTank('LFB Twin-Boar', RadialSize.Large, 5750, 36000)

class FuelTypes(Enum):
    LiquidFuel = ('Liquid fuel (+Oxidizer)', 5)
    AtomicFuel = ('Atomic fuel', 5)    # atomic fuel is liquid fuel w/out oxidizer
    Xenon = ('Xenon', 0.1)
    Monopropellant = ('Monopropellant', 4)
    def __init__(self, pname, unitmass):
        self.pname = pname
        self.unitmass = unitmass

SpecialEngine = namedtuple('SpecialEngine', ['size', 'name', 'cost', 'm', 'isp_atm', 'isp_vac',
    'F_vac', 'tvc', 'level', 'electricity', 'length'])
SpecialFuelTank = namedtuple('SpecialFuelTank', ['name', 'size', 'cost', 'm_full', 'f_e', 'level'])

AtomicRocketMotor = SpecialEngine(RadialSize.Small, 'LV-N Nerv Atomic Rocket Motor', 10000, 3000,
        185, 800, 60000, 0, ResearchNode.NuclearPropulsion, 1, 3)
AtomicTankFactor = 23/45    # Mass of full atomic fuel tank / Mass of full liquid fuel tank
AtomicTank_f_e = 5/18

ElectricPropulsionSystem = SpecialEngine(RadialSize.Tiny, 'IX-6315 Dawn Electric Propulsion System',
        8000, 250, 100, 4200, 2000, 0, ResearchNode.IonPropulsion, 0, 0)
XenonTanks = [
    SpecialFuelTank('PB-X150', RadialSize.Tiny, 3600, 125, 11/14, ResearchNode.IonPropulsion),
    SpecialFuelTank('PB-X750', RadialSize.Small, 22500, 937.5, 11/14, ResearchNode.IonPropulsion),
    SpecialFuelTank('PB-X50R', RadialSize.RadiallyMounted, 2200, 71.4, 157/200, ResearchNode.IonPropulsion) ]

MonoPropellantEngine = \
        SpecialEngine(RadialSize.RadiallyMounted, 'O-10 Puff MonoPropellant Fuel Engine',
                      150, 90, 120, 250, 20000, 0, ResearchNode.PrecisionPropulsion, 0, 0)
MonoPropellantTanks = [
        SpecialFuelTank('FL-R10', RadialSize.Tiny,  200,  370, 5/32, ResearchNode.AdvancedFuelSystems),
        SpecialFuelTank('FL-R25', RadialSize.Small, 600,  1150, 3/20, ResearchNode.AdvancedFuelSystems),
        SpecialFuelTank('FL-R1',  RadialSize.Large, 1300, 3400, 2/15, ResearchNode.AdvancedFuelSystems),
        SpecialFuelTank('Stratus-V Roundified', RadialSize.RadiallyMounted, 200, 315, 5 / 16,
                        ResearchNode.AdvancedFlightControl),
        SpecialFuelTank('Stratus-V Cylindrified', RadialSize.RadiallyMounted, 450, 750, 1 / 4,
                        ResearchNode.SpecializedControl) ]

SolidFuelBooster = namedtuple('SolidFuelBooster', ['name', 'cost', 'm_full', 'm_empty', 'isp_atm', 'isp_vac', 'F_vac', 'level'])

SolidFuelBoosters = [
        SolidFuelBooster('RT-5 Flea',    200,  1500,  450,  140, 165, 192000, ResearchNode.Start),
        SolidFuelBooster('RT-10 Hammer', 400,  3560,  750,  170, 195, 227000, ResearchNode.BasicRocketry),
        SolidFuelBooster('BACC Thumper', 850,  7650,  1500, 175, 210, 300000, ResearchNode.GeneralRocketry),
        SolidFuelBooster('S1 Kickback',  2700, 24000, 4500, 195, 220, 670000, ResearchNode.HeavyRocketry) ]

# Extra for stacked stage
StackstageExtraMass = 50
StackstageExtraCost = 400
StackstageExtraNote = "TR-18A Stack Decoupler"
StackstageExtraTech = ResearchNode.Engineering101

# Extra for radial stage
# This is a very heavy but safe default setting, also having the effect of
# reducing the count of extra radial solid fuel boosters.
RadialstageExtraMass = 50 + 75 + 2*50
RadialstageExtraCost = 700 + 320 + 2*42
RadialstageExtraNote = "TT-70 Radial Decoupler, Advanced Nose Cone, 2 * EAS-4 Strut Connector"
RadialstageExtraTech = ResearchNode.Stability   # actually TT-70 is not there, but with TT-38K the
                                                # first radial decoupler.
<|endoftext|>


<commit_before>import os
import shutil
from PIL import Image

# TODO: All this stuff needs to be refactored as a class and have a better speed handling

def get_output_path(article):
    '''
    Return an output path

    :param article:
    :return:
    '''
    # TODO: DONT!
    return os.path.join(os.path.abspath("."), "output")

def make_header_image(path):
    '''
    Resize image to the restrictions.

    :param path:
    :return:
    '''
    size = 730, 730
    if os.path.exists(path):
        im = Image.open(path)
        im.thumbnail(size, Image.ANTIALIAS)
        w, h = im.size
        if h > 292:
            cut_h = (h - 292) / 2
            im.crop((0, cut_h, w, h - cut_h)).save(path, "JPEG")
        else:
            im.save(path, "JPEG")

def save_image(article):
    '''
    Save image to the article path.

    :param path:
    :return:
    '''
    article.web_path = os.path.join(os.path.dirname(article.save_as),
                                    article.settings.get('ARTICLE_PICTURES'),
                                    os.path.basename(article.header_image_src))
    header_image_save_as = os.path.join(get_output_path(article), article.web_path)
    if not os.path.exists(article.header_image_src):
        raise Exception('File "{0}" does not exists!'.format(article.header_image_src))
    if not os.path.exists(header_image_save_as):
        if not os.path.exists(os.path.dirname(header_image_save_as)):
            os.makedirs(os.path.dirname(header_image_save_as))
        shutil.copy(article.header_image_src, header_image_save_as)
        make_header_image(header_image_save_as)

def get_source_post_image(article):
    '''
    Get a source image for the post

    :param article:
    :return:
    '''
    all_images = article.settings.get('ARTICLE_PICTURES')
    default_image = article.settings.get('ARTICLE_DEFAULT_PICTURE')
    if not all_images or not default_image:
        raise Exception('This plugin requires that ARTICLE_PICTURES and ARTICLE_DEFAULT_PICTURE should be defined!')
    a_img_path = os.path.join(os.path.dirname(article.source_path), all_images)

    # First see if there is an explicit image filename in the article metadata
    article_image = None
    article_default_image_no_ext = ".".join(os.path.basename(article.source_path).split(".")[:-1])
    if hasattr(article, 'image'):
        article_image = os.path.join(a_img_path, os.path.sep.join(
            [pth for pth in article.image.split(os.path.sep) if pth]))

    # See if there is an image, named after article filename with the jpeg/jpg/png extensions
    if not article_image:
        for f_ext in ['jpeg', 'jpg', 'png']:
            article_image = os.path.join(a_img_path, '{0}.{1}'.format(article_default_image_no_ext, f_ext))
            if not os.path.exists(article_image):
                article_image = None
            else:
                break

    # Set just a default image
    if not article_image:
        article_image = os.path.join(a_img_path, default_image)
        article.default_image = True
    else:
        article.default_image = False

    return article_image

def contentor(generator):
    '''
    Create slugs by file creation
    '''
    for article in generator.articles:
        article.formatted_date = article.date.strftime("%Y.%m.%d")
        article.lead = article.content.split('
')[0].replace('
', '')
        article.content_no_lead = "
".join(article.content.split('
')[1:])
        if not getattr(article, "_summary", None):
            article._summary = ""

        if hasattr(article, 'gallery'):
            article.template = 'gallery'
            continue

        if not getattr(article, "noimage", None):  # noimage: True  -- disables the header image completely
            article.header_image_src = get_source_post_image(article)
            save_image(article)

def register():
    '''
    Registration.
    '''
    from pelican import signals
    signals.article_generator_finalized.connect(contentor)
<commit_msg>Implement video tag rendering<commit_after>import os
import shutil
from PIL import Image
from pelican import signals, contents

# TODO: All this stuff needs to be refactored as a class and have a better speed handling

def get_output_path(article):
    '''
    Return an output path

    :param article:
    :return:
    '''
    # TODO: DONT!
    return os.path.join(os.path.abspath("."), "output")

def make_header_image(path):
    '''
    Resize image to the restrictions.

    :param path:
    :return:
    '''
    size = 730, 730
    if os.path.exists(path):
        im = Image.open(path)
        im.thumbnail(size, Image.ANTIALIAS)
        w, h = im.size
        if h > 292:
            cut_h = (h - 292) / 2
            im.crop((0, cut_h, w, h - cut_h)).save(path, "JPEG")
        else:
            im.save(path, "JPEG")

def save_image(article):
    '''
    Save image to the article path.

    :param path:
    :return:
    '''
    article.web_path = os.path.join(os.path.dirname(article.save_as),
                                    article.settings.get('ARTICLE_PICTURES'),
                                    os.path.basename(article.header_image_src))
    header_image_save_as = os.path.join(get_output_path(article), article.web_path)
    if not os.path.exists(article.header_image_src):
        raise Exception('File "{0}" does not exists!'.format(article.header_image_src))
    if not os.path.exists(header_image_save_as):
        if not os.path.exists(os.path.dirname(header_image_save_as)):
            os.makedirs(os.path.dirname(header_image_save_as))
        shutil.copy(article.header_image_src, header_image_save_as)
        make_header_image(header_image_save_as)

def get_source_post_image(article):
    '''
    Get a source image for the post

    :param article:
    :return:
    '''
    all_images = article.settings.get('ARTICLE_PICTURES')
    default_image = article.settings.get('ARTICLE_DEFAULT_PICTURE')
    if not all_images or not default_image:
        raise Exception('This plugin requires that ARTICLE_PICTURES and ARTICLE_DEFAULT_PICTURE should be defined!')
    a_img_path = os.path.join(os.path.dirname(article.source_path), all_images)

    # First see if there is an explicit image filename in the article metadata
    article_image = None
    article_default_image_no_ext = ".".join(os.path.basename(article.source_path).split(".")[:-1])
    if hasattr(article, 'image'):
        article_image = os.path.join(a_img_path, os.path.sep.join(
            [pth for pth in article.image.split(os.path.sep) if pth]))

    # See if there is an image, named after article filename with the jpeg/jpg/png extensions
    if not article_image:
        for f_ext in ['jpeg', 'jpg', 'png']:
            article_image = os.path.join(a_img_path, '{0}.{1}'.format(article_default_image_no_ext, f_ext))
            if not os.path.exists(article_image):
                article_image = None
            else:
                break

    # Set just a default image
    if not article_image:
        article_image = os.path.join(a_img_path, default_image)
        article.default_image = True
    else:
        article.default_image = False

    return article_image

def contentor(generator):
    '''
    Create slugs by file creation
    '''
    for article in generator.articles:
        article.formatted_date = article.date.strftime("%Y.%m.%d")
        article.lead = article.content.split('
')[0].replace('
', '')
        article.content_no_lead = "
".join(article.content.split('
')[1:])
        if not getattr(article, "_summary", None):
            article._summary = ""

        if hasattr(article, 'gallery'):
            article.template = 'gallery'
            continue

        if not getattr(article, "noimage", None):  # noimage: True  -- disables the header image completely
            article.header_image_src = get_source_post_image(article)
            save_image(article)

class ArticleContentParser(object):
    def __init__(self, content):
        self.content = content

    def parse(self):
        if not isinstance(self.content, contents.Article):
            return
        self.render_vimeo_tags()

    def _untag(self, data):
        out = list()
        for elm in data.split(">"):
            elm = elm.split("<")[0]
            if elm:
                out.append(elm)

        return ' '.join(out)

    def _render_youtube_tag(self, tag):
        '''
        Render YouTube tag.

        :param tag:
        :return:
        '''
        template = """

"""
        return template.format(video_id=tag.split("{youtube}")[-1])

    def _render_vimeo_tag(self, tag):
        '''
        Render Vimeo tag.

        :param tag:
        :return:
        '''
        template = """

"""
        return template.format(video_id=tag.split("{vimeo}")[-1])

    def render_vimeo_tags(self):
        '''
        Render video tags for Vimeo player.
        :return:
        '''
        body = list()
        for line in (self.content._content or '').split(os.linesep):
            if line.find('{vimeo}') > -1:
                line = self._render_vimeo_tag(self._untag(line))
            elif line.find('{youtube}') > -1:
                line = self._render_youtube_tag(self._untag(line))
            body.append(line)
        self.content._content = os.linesep.join(body)

def article_content_parser(content):
    ArticleContentParser(content).parse()

def register():
    '''
    Registration.
    '''
    signals.content_object_init.connect(article_content_parser)
    signals.article_generator_finalized.connect(contentor)
<|endoftext|>


<commit_before>import numpy as np

from sklearn.feature_extraction.text import CountVectorizer as CV
from sklearn.naive_bayes import MultinomialNB as MNB

from streamScript.domain.send_data import query_all_db, query_all_db_Tweet200
import picklers

u"""Generates a vocabulary
set and builds a feature matrix. Creates a classifier and returns
cross-validated predictions. Pickles dataset and matrix as necessary."""

def build_test_matrix(user_data, vocab):
    u"""Takes in a list of lists, with each list containing tuples
    representing tweets from a single user, and a vocab list. Returns an X
    matrix of the test user features, a list of the user names, and a Y
    array of the labels."""
    matrix = []
    user_array = []
    user_cities = []
    #print user_data
    for history in user_data:
        #print history
        user_string = ""
        user_name = history[0][0]
        user_array.append(user_name)
        user_cities.append(history[0][5])
        for tweet in history:
            if history[0][0] == user_name:
                user_string += tweet[1].lower()
        matrix.append(user_string)
    vec = CV(
        analyzer='word',
        vocabulary=vocab
    )
    print "Building test X, Y..."
    X = vec.fit_transform(matrix, vocab).todense()
    # print X
    # print user_array
    # print user_cities
    return X, user_array, user_cities

def vectorize(user_matrix, user_array, n):
    stopwords = open('text/stopwords.txt').read().lower().split()
    vec = CV(
        analyzer='word',
        stop_words=stopwords,
        max_features=n,
    )
    print "Building X, Y..."
    X = vec.fit_transform(user_matrix).toarray()
    Y = np.array(user_array)
    return X, Y, vec.get_feature_names()

def build_matrix(data, n=1000):
    u"""Uses blocks of tweets from multiple users per city.
    Takes in a raw dataset and an optional parameter to limit the feature
    set to n. Defaults to 1000. Returns a tuple containing a matrix of n features,
    a vector of labels, and a vocabulary list of the features examined."""
    user_matrix = []
    user_array = []
    tweet_count = 0
    for key, val in data.items():
        for tweet in val:
            if not tweet_count % 200:
                user_array.append(key)
                user_matrix.append(" ")
            user_matrix[-1] += tweet[2].lower()
            tweet_count += 1
    return vectorize(user_matrix, user_array, n)

def build_matrix_per_user(data, n=1000):
    u""" Uses blocks of tweets from single users per city.
    Takes in a raw dataset and an optional parameter to limit the feature
    set to n. Defaults to 1000. Returns a tuple containing a matrix of n features,
    a vector of labels, and a vocabulary list of the features examined."""
    user_matrix = []
    user_array = []
    for key, val in data.items():
        count = 0
        for tweet in val:
            if count == 0:
                this_user = tweet[0]
                our_string = ""
            if (tweet[0] == this_user) and (count < 200):
                our_string += tweet[2].lower()
                count += 1
            elif (tweet[0] != this_user) and (len(user_matrix[-1]) >= 14000):
                count = 0
                user_matrix.append(our_string)
                user_array.append(key)
            elif tweet[0] != this_user:
                count = 0
    return vectorize(user_matrix, user_array, n)

def fit_classifier(X, y):
    u"""Takes in an X matrix and a Y array of labels.
    Fits classifier"""
    mnb = MNB()
    return mnb.fit(X, y)

def get_raw_classifier(make_new_pickles=False, readpickle=True, useTweet200=False):
    u"""Takes in keyword arguments to determine to source of data. Returns a
    trained classifier."""
    if readpickle:
        X = picklers.load_matrix_pickle()
        y = picklers.load_y_pickle()
        data = picklers.load_data_pickle()
    elif useTweet200:
        data = query_all_db_Tweet200()
        X, y, vocab = build_matrix_per_user(data)
    else:
        data = query_all_db(limit=True)
        X, y, vocab = build_matrix(data)
    mnb = fit_classifier(X, y)
    if make_new_pickles:
        picklers.pickle_classifier(mnb)
    if not readpickle:
        picklers.pickle_data(data)
        picklers.pickle_matrix(X)
        picklers.pickle_labels(y)
        picklers.pickle_vocab(vocab)
    print "returning mnb"
    return mnb

def generate_predictions(userTestdata):
    u"""Takes in a list of twitter users' last 200 tweets, formatted as
    'blobs'. Returns a percent correct (if known), a list of all incorrect guesses
    (or unknown), and a list of all the city predictions."""
    mnb = picklers.load_classifier_pickle()
    vocab = picklers.load_vocab_pickle()
    X, user_array, user_cities = build_test_matrix(userTestdata, vocab)
    correct = 0
    incorrect = 0
    got_wrong = []
    all_results = []
    predictions = mnb.predict_log_proba(X)
    if len(predictions):
        for idx, prediction in enumerate(predictions):
            report = (user_array[idx], user_cities[idx], prediction)
            if user_cities[idx] == prediction:
                correct += 1
            else:
                incorrect += 1
                got_wrong.append(report)
            all_results.append(report)
        percent_right = correct / (float(correct) + incorrect)
        return percent_right, got_wrong, all_results

if __name__ == "__main__":
    print get_raw_classifier(make_new_pickles=True, readpickle=False, useTweet200=False)
<commit_msg>changed some variable names<commit_after>import numpy as np

from sklearn.feature_extraction.text import CountVectorizer as CV
from sklearn.naive_bayes import MultinomialNB as MNB

from streamScript.domain.send_data import query_all_db, query_all_db_Tweet200
import picklers

u"""Generates a vocabulary
set and builds a feature matrix. Creates a classifier and returns
cross-validated predictions. Pickles dataset and matrix as necessary."""

def build_test_matrix(user_data, vocab):
    u"""Takes in a list of lists, with each list containing tuples
    representing tweets from a single user, and a vocab list. Returns an X
    matrix of the test user features, a list of the user names, and a Y
    array of the labels."""
    matrix = []
    user_array = []
    user_cities = []
    #print user_data
    for history in user_data:
        #print history
        user_string = ""
        user_name = history[0][0]
        user_array.append(user_name)
        user_cities.append(history[0][5])
        for tweet in history:
            if history[0][0] == user_name:
                user_string += tweet[1].lower()
        matrix.append(user_string)
    vec = CV(
        analyzer='word',
        vocabulary=vocab
    )
    print "Building test X, Y..."
    X = vec.fit_transform(matrix, vocab).todense()
    # print X
    # print user_array
    # print user_cities
    return X, user_array, user_cities

def vectorize(user_matrix, user_array, n):
    stopwords = open('text/stopwords.txt').read().lower().split()
    vec = CV(
        analyzer='word',
        stop_words=stopwords,
        max_features=n,
    )
    print "Building X, Y..."
    X = vec.fit_transform(user_matrix).toarray()
    Y = np.array(user_array)
    return X, Y, vec.get_feature_names()

def build_matrix(data, n=1000):
    u"""Uses blocks of tweets from multiple users per city.
    Takes in a raw dataset and an optional parameter to limit the feature
    set to n. Defaults to 1000. Returns a tuple containing a matrix of n features,
    a vector of labels, and a vocabulary list of the features examined."""
    user_matrix = []
    user_array = []
    tweet_count = 0
    for key, val in data.items():
        for tweet in val:
            if not tweet_count % 200:
                user_array.append(key)
                user_matrix.append(" ")
            user_matrix[-1] += tweet[2].lower()
            tweet_count += 1
    return vectorize(user_matrix, user_array, n)

def build_matrix_per_user(data, n=1000):
    u""" Uses blocks of tweets from single users per city.
    Takes in a raw dataset and an optional parameter to limit the feature
    set to n. Defaults to 1000. Returns a tuple containing a matrix of n features,
    a vector of labels, and a vocabulary list of the features examined."""
    user_matrix = []
    user_array = []
    for key, val in data.items():
        count = 0
        for tweet in val:
            if count == 0:
                this_user = tweet[0]
                our_string = ""
            if (tweet[0] == this_user) and (count < 200):
                our_string += tweet[2].lower()
                count += 1
            elif (tweet[0] != this_user) and (len(user_matrix[-1]) >= 14000):
                count = 0
                user_matrix.append(our_string)
                user_array.append(key)
            elif tweet[0] != this_user:
                count = 0
    return vectorize(user_matrix, user_array, n)

def fit_classifier(X, y):
    u"""Takes in an X matrix and a Y array of labels.
    Fits classifier"""
    mnb = MNB()
    return mnb.fit(X, y)

def get_raw_classifier(make_new_pickles=False, read_pickles=True, useTweet200=False):
    u"""Takes in keyword arguments to determine to source of data. Returns a
    trained classifier."""
    if read_pickles:
        X = picklers.load_matrix_pickle()
        y = picklers.load_y_pickle()
        data = picklers.load_data_pickle()
    elif useTweet200:
        data = query_all_db_Tweet200()
        X, y, vocab = build_matrix_per_user(data)
    else:
        data = query_all_db(limit=True)
        X, y, vocab = build_matrix(data)
    mnb = fit_classifier(X, y)
    if make_new_pickles:
        picklers.pickle_classifier(mnb)
        if not read_pickles:
            picklers.pickle_data(data)
            picklers.pickle_matrix(X)
            picklers.pickle_labels(y)
            picklers.pickle_vocab(vocab)
    print "returning mnb"
    return mnb

def generate_predictions(userTestdata):
    u"""Takes in a list of twitter users' last 200 tweets, formatted as
    'blobs'. Returns a percent correct (if known), a list of all incorrect guesses
    (or unknown), and a list of all the city predictions."""
    mnb = picklers.load_classifier_pickle()
    vocab = picklers.load_vocab_pickle()
    X, user_array, user_cities = build_test_matrix(userTestdata, vocab)
    correct = 0
    incorrect = 0
    got_wrong = []
    all_results = []
    predictions = mnb.predict_log_proba(X)
    if len(predictions):
        for idx, prediction in enumerate(predictions):
            report = (user_array[idx], user_cities[idx], prediction)
            if user_cities[idx] == prediction:
                correct += 1
            else:
                incorrect += 1
                got_wrong.append(report)
            all_results.append(report)
        percent_right = correct / (float(correct) + incorrect)
        return percent_right, got_wrong, all_results

if __name__ == "__main__":
    print get_raw_classifier(make_new_pickles=True, read_pickles=False, useTweet200=False)
<|endoftext|>


<commit_before>"""
Copyright (c) 2013, Dave Mankoff
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of Dave Mankoff nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DAVE MANKOFF BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

from __future__ import unicode_literals
import sys

import re
try:
  from html.parser import HTMLParser
except ImportError:
  from HTMLParser import HTMLParser

from . import escape

PRE_TAGS = ('pre', 'textarea')  # styles and scripts are never minified
# http://www.w3.org/TR/html51/syntax.html#elements-0
NO_CLOSE_TAGS = ('area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img',
                 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track',
                 'wbr')
# http://www.w3.org/TR/html51/index.html#attributes-1
BOOLEAN_ATTRIBUTES = {
  'audio': ('autoplay', 'controls', 'loop', 'muted',),
  'button': ('autofocus', 'disabled', 'formnovalidate',),
  'command': ('checked', 'disabled',),
  'dialog': ('open',),
  'fieldset': ('disabled',),
  'form': ('novalidate',),
  'iframe': ('seamless',),
  'img': ('ismap',),
  'input': ('autofocus', 'checked', 'disabled', 'formnovalidate', 'multiple',
            'readonly', 'required',),
  'keygen': ('autofocus', 'disabled',),
  'object': ('typesmustmatch',),
  'ol': ('reversed',),
  'optgroup': ('disabled',),
  'option': ('disabled', 'selected',),
  'script': ('async', 'defer'),
  'select': ('autofocus', 'disabled', 'multiple', 'required',),
  'style': ('scoped',),
  'textarea': ('autofocus', 'disabled', 'readonly', 'required',),
  'track': ('default',),
  'video': ('autoplay', 'controls', 'loop', 'muted',),
  '*': ('hidden',),
}

leading_whitespace_re = re.compile(r'^\s+')
trailing_whitespace_re = re.compile(r'\s+$')
leading_trailing_whitespace_re = re.compile(r'(^\s+)|(\s+$)')
whitespace_re = re.compile(r'\s+')
whitespace_newline_re = re.compile(r'\s*(\r|\n)+\s*')

# Tag omission rules:
# http://www.w3.org/TR/html51/syntax.html#optional-tags

class HTMLMinError(Exception): pass
class ParseError(HTMLMinError): pass
class OpenTagNotFoundError(ParseError): pass

class HTMLMinParser(HTMLParser):
  def __init__(self,
               remove_comments=False,
               remove_empty_space=False,
               remove_all_empty_space=False,
               reduce_empty_attributes=True,
               reduce_boolean_attributes=False,
               remove_optional_attribute_quotes=True,
               keep_pre=False,
               pre_tags=PRE_TAGS,
               pre_attr='pre'):
    if sys.version_info[0] >= 3 and sys.version_info[1] >= 4:
      # convert_charrefs is True by default in Python 3.5.0 and newer. It was
      # introduced in 3.4.
      HTMLParser.__init__(self, convert_charrefs=False)
    else:
      HTMLParser.__init__(self)
    self.keep_pre = keep_pre
    self.pre_tags = pre_tags
    self.remove_comments = remove_comments
    self.remove_empty_space = remove_empty_space
    self.remove_all_empty_space = remove_all_empty_space
    self.reduce_empty_attributes = reduce_empty_attributes
    self.reduce_boolean_attributes = reduce_boolean_attributes
    self.remove_optional_attribute_quotes = remove_optional_attribute_quotes
    self.pre_attr = pre_attr
    self._data_buffer = []
    self._in_pre_tag = 0
    self._in_head = False
    self._in_title = False
    self._after_doctype = False
    self._tag_stack = []
    self._title_newly_opened = False
    self.__title_trailing_whitespace = False

  def _has_pre(self, attrs):
    for k,v in attrs:
      if k == self.pre_attr:
        return True
    return False

  def build_tag(self, tag, attrs, close_tag):
    last_quoted = -1
    last_no_slash = -1

    if self.reduce_boolean_attributes:
      bool_attrs = (BOOLEAN_ATTRIBUTES.get(tag, ()), BOOLEAN_ATTRIBUTES['*'])
      reduce_boolean = lambda k: k in bool_attrs[0] or k in bool_attrs[1]
    else:
      reduce_boolean = lambda k: False

    attrs = list(attrs)  # We're modifying it in place
    for i, (k, v) in enumerate(attrs):
      k = escape.escape_attr_name(k)
      if (not v and self.reduce_empty_attributes) or reduce_boolean(k):
        # For our use case, we treat boolean attributes as quoted because they
        # don't require space between them and "/>" in closing tags.
        attrs[i] = k
        last_quoted = i
      else:
        (v, q) = escape.escape_attr_value(
          v, double_quote=not self.remove_optional_attribute_quotes)
        if q == escape.NO_QUOTES:
          attrs[i] = '%s=%s' % (k, v)
          if v[-1] != '/':
            last_no_slash = i
        else:
          q = '"' if q == escape.DOUBLE_QUOTE else "'"
          attrs[i] = '%s=%s%s%s' % (k, q, v, q)
          last_quoted = i

    # 1. If there are no attributes, no additional space is necessary.
    # 2. If last attribute is quoted, no additional space is necessary.
    # 3. Two things are happening here:
    #    a) according to the standard, <foo bar=baz/> should be treated as <foo
    #       bar="baz/"> so space is necessary if this is self-closing tag,
    #       however
    #    b) reportedly (https://github.com/mankyd/htmlmin/pull/12), older
    #       versions of WebKit interpret <foo bar=baz/> as self-closing tag so
    #       we need the space if the last argument ends with a slash.
    space_maybe = ''
    if attrs:
      needs_space = lambda last_attr: (last_attr[-1] not in '"\'' and
                                       (close_tag or last_attr[-1] == '/'))
      if needs_space(attrs[-1][-1]):
        # If moving attributes around can help, do it.  Otherwise bite the
        # bullet and put the space in.
        i = last_no_slash if last_quoted == -1 else last_quoted
        if i == -1 or needs_space(attrs[i]):
          space_maybe = ' '
        else:
          attrs.append(attrs[i])
          del attrs[i]

    return '<%s%s%s%s%s>' % (escape.escape_tag(tag),
                             ' ' if attrs else '',
                             ' '.join(attrs),
                             space_maybe,
                             '/' if close_tag else '')

  def handle_decl(self, decl):
    if (len(self._data_buffer) == 1 and
        whitespace_re.match(self._data_buffer[0])):
      self._data_buffer = []
    self._data_buffer.append('<!' + decl + '>')
    self._after_doctype = True

  def in_tag(self, *tags):
    for t in self._tag_stack:
      if t[0] in tags:
        return t
    return False

  def _close_tags_up_to(self, tag):
    num_pres = 0
    i = 0
    for i, t in enumerate(self._tag_stack):
      if t[1]:
        num_pres += 1
      if t[0] == tag:
        break

      # Only the html tag can close out everything. Put on the brakes if
      # we encounter a closing tag that we didn't recognize.
      if tag != 'html' and t[0] in ('body', 'html', 'head'):
        raise OpenTagNotFoundError()

    self._tag_stack = self._tag_stack[i+1:]

    return num_pres

  def handle_starttag(self, tag, attrs):
    self._after_doctype = False
    if tag == 'head':
      self._in_head = True
    elif self._in_head and tag == 'title':
      self._in_title = True
      self._title_newly_opened = True

    tag_sets = ( # a list of tags and tags that they are closed by
      (('li',), ('li',)),
      (('dd', 'dt',), ('dd', 'dt',)),
      (('rp', 'rt',), ('rp', 'rt',)),
      (('p',), ('address', 'article', 'aside', 'blockquote', 'dir', 'div',
                'dl', 'fieldset', 'footer', 'form', 'h1', 'h2', 'h3', 'h4',
                'h5', 'h6', 'header', 'hgroup', 'hr', 'menu', 'nav', 'ol', 'p',
                'pre', 'section', 'table', 'ul')),
      (('optgroup',), ('optgroup',)),
      (('option',), ('option', 'optgroup')),
      (('colgroup',), ('*',)),
      (('tbody', 'thead',), ('tbody', 'tfoot')),
      (('tfoot',), ('tbody',)),
      (('tr',), ('tr',)),
      (('td', 'th'), ('td', 'th')),
      )
    for open_tags, closed_by_tags in tag_sets:
      in_tag = self.in_tag(*open_tags)
      if in_tag and (tag in closed_by_tags or '*' in closed_by_tags):
        self._in_pre_tag -= self._close_tags_up_to(in_tag[0])

    start_pre = False
    if (tag in self.pre_tags or
        tag in ('script', 'style') or
        self._has_pre(attrs) or
        self._in_pre_tag > 0):
      self._in_pre_tag += 1
      start_pre = True

    self._tag_stack.insert(0, (tag, start_pre))

    if not self.keep_pre:
      attrs = [(k,v) for k,v in attrs if k != self.pre_attr]

    self._data_buffer.append(self.build_tag(tag, attrs, False))

  def handle_endtag(self, tag):
    # According to the spec,
 tags don't get closed when a parent a
    # tag closes them. Here's some logic that addresses this.
    if tag == 'a':
      contains_p = False
      for i, t in enumerate(self._tag_stack):
        if t[0] == 'p':
          contains_p = True
        elif t[0] == 'a':
          break
      if contains_p: # the p tag, and all its children should be left open
        a_tag = self._tag_stack.pop(i)
        if a_tag[1]:
          self._in_pre_tag -= 1
    else:
      if tag == 'head':
        # TODO: Did we know that we were in an head tag?! If not, we need to
        # reminify everything to remove extra spaces.
        self._in_head = False
      elif tag == 'title':
        self._in_title = False
        self._title_newly_opened = False
      try:
        self._in_pre_tag -= self._close_tags_up_to(tag)
      except OpenTagNotFoundError:
        # Some tags don't require a start tag. Most do. Either way, we leave
        # closing tags along since they affect output. For instance, a '
'
        # results in a '

' in Chrome.
        pass
    if tag not in NO_CLOSE_TAGS:
      self._data_buffer.extend(['</', escape.escape_tag(tag), '>'])

  def handle_startendtag(self, tag, attrs):
    self._after_doctype = False
    if not self.keep_pre:
      attrs = [(k,v) for k,v in attrs if k != 'pre']
    self._data_buffer.append(self.build_tag(tag, attrs, tag not in NO_CLOSE_TAGS))

  def handle_comment(self, data):
    if not self.remove_comments or (
        data and (data[0] == '!' or re.match(r'^\[if\s', data))):
      self._data_buffer.append(''.format(
          data[1:] if len(data) and data[0] == '!' else data))

  def handle_data(self, data):
    if self._in_pre_tag > 0:
      self._data_buffer.append(data)
    else:
      # remove_all_empty_space matches everything. remove_empty_space only
      # matches if there's a newline involved.
      if self.remove_all_empty_space or self._in_head or self._after_doctype:
        match = whitespace_re.match(data)
        if match and match.end(0) == len(data):
          return
      elif self.remove_empty_space:
        match = whitespace_newline_re.match(data)
        if match and match.end(0) == len(data):
          return

      # if we're in the title, remove leading and trailing whitespace.
      # note that the title may be parsed in chunks if entityref's or charrefs
      # are encountered.
      if self._in_title:
        if self.__title_trailing_whitespace:
          self._data_buffer.append(' ')
        self.__title_trailing_whitespace = data[-1].isspace()
        if self._title_newly_opened:
          self._title_newly_opened = False
          data = leading_trailing_whitespace_re.sub('', data)
        else:
          data = trailing_whitespace_re.sub(
            '', leading_whitespace_re.sub(' ', data))

      data = whitespace_re.sub(' ', data)
      if not data:
        return

      if self._in_pre_tag == 0 and self._data_buffer:
        # If we're not in a pre block, its possible that we append two spaces
        # together, which we want to avoid. For instance, if we remove a comment
        # from between two blocks of text: a  c => a  c.
        if data[0] == ' ' and self._data_buffer[-1][-1] == ' ':
          data = data[1:]
          if not data:
            return
      self._data_buffer.append(data)

  def handle_entityref(self, data):
    if self._in_title:
      if not self._title_newly_opened and self.__title_trailing_whitespace:
        self._data_buffer.append(' ')
        self.__title_trailing_whitespace = False
      self._title_newly_opened = False
    self._data_buffer.append('&{};'.format(data))

  def handle_charref(self, data):
    if self._in_title:
      if not self._title_newly_opened and self.__title_trailing_whitespace:
        self._data_buffer.append(' ')
        self.__title_trailing_whitespace = False
      self._title_newly_opened = False
    self._data_buffer.append('&#{};'.format(data))

  def handle_pi(self, data):
    self._data_buffer.append('<?' + data + '>')

  def unknown_decl(self, data):
    self._data_buffer.append('<![' + data + ']>')

  def reset(self):
    self._data_buffer = []
    HTMLParser.reset(self)

  @property
  def result(self):
    return ''.join(self._data_buffer)
<commit_msg>Don’t add empty value to boolean attributes<commit_after>"""
Copyright (c) 2013, Dave Mankoff
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of Dave Mankoff nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DAVE MANKOFF BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

from __future__ import unicode_literals
import sys

import re
try:
  from html.parser import HTMLParser
except ImportError:
  from HTMLParser import HTMLParser

from . import escape

PRE_TAGS = ('pre', 'textarea')  # styles and scripts are never minified
# http://www.w3.org/TR/html51/syntax.html#elements-0
NO_CLOSE_TAGS = ('area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img',
                 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track',
                 'wbr')
# http://www.w3.org/TR/html51/index.html#attributes-1
BOOLEAN_ATTRIBUTES = {
  'audio': ('autoplay', 'controls', 'loop', 'muted',),
  'button': ('autofocus', 'disabled', 'formnovalidate',),
  'command': ('checked', 'disabled',),
  'dialog': ('open',),
  'fieldset': ('disabled',),
  'form': ('novalidate',),
  'iframe': ('seamless',),
  'img': ('ismap',),
  'input': ('autofocus', 'checked', 'disabled', 'formnovalidate', 'multiple',
            'readonly', 'required',),
  'keygen': ('autofocus', 'disabled',),
  'object': ('typesmustmatch',),
  'ol': ('reversed',),
  'optgroup': ('disabled',),
  'option': ('disabled', 'selected',),
  'script': ('async', 'defer'),
  'select': ('autofocus', 'disabled', 'multiple', 'required',),
  'style': ('scoped',),
  'textarea': ('autofocus', 'disabled', 'readonly', 'required',),
  'track': ('default',),
  'video': ('autoplay', 'controls', 'loop', 'muted',),
  '*': ('hidden',),
}

leading_whitespace_re = re.compile(r'^\s+')
trailing_whitespace_re = re.compile(r'\s+$')
leading_trailing_whitespace_re = re.compile(r'(^\s+)|(\s+$)')
whitespace_re = re.compile(r'\s+')
whitespace_newline_re = re.compile(r'\s*(\r|\n)+\s*')

# Tag omission rules:
# http://www.w3.org/TR/html51/syntax.html#optional-tags

class HTMLMinError(Exception): pass
class ParseError(HTMLMinError): pass
class OpenTagNotFoundError(ParseError): pass

class HTMLMinParser(HTMLParser):
  def __init__(self,
               remove_comments=False,
               remove_empty_space=False,
               remove_all_empty_space=False,
               reduce_empty_attributes=True,
               reduce_boolean_attributes=False,
               remove_optional_attribute_quotes=True,
               keep_pre=False,
               pre_tags=PRE_TAGS,
               pre_attr='pre'):
    if sys.version_info[0] >= 3 and sys.version_info[1] >= 4:
      # convert_charrefs is True by default in Python 3.5.0 and newer. It was
      # introduced in 3.4.
      HTMLParser.__init__(self, convert_charrefs=False)
    else:
      HTMLParser.__init__(self)
    self.keep_pre = keep_pre
    self.pre_tags = pre_tags
    self.remove_comments = remove_comments
    self.remove_empty_space = remove_empty_space
    self.remove_all_empty_space = remove_all_empty_space
    self.reduce_empty_attributes = reduce_empty_attributes
    self.reduce_boolean_attributes = reduce_boolean_attributes
    self.remove_optional_attribute_quotes = remove_optional_attribute_quotes
    self.pre_attr = pre_attr
    self._data_buffer = []
    self._in_pre_tag = 0
    self._in_head = False
    self._in_title = False
    self._after_doctype = False
    self._tag_stack = []
    self._title_newly_opened = False
    self.__title_trailing_whitespace = False

  def _has_pre(self, attrs):
    for k,v in attrs:
      if k == self.pre_attr:
        return True
    return False

  def build_tag(self, tag, attrs, close_tag):
    last_quoted = -1
    last_no_slash = -1

    if self.reduce_boolean_attributes:
      bool_attrs = (BOOLEAN_ATTRIBUTES.get(tag, ()), BOOLEAN_ATTRIBUTES['*'])
      reduce_boolean = lambda k: k in bool_attrs[0] or k in bool_attrs[1]
    else:
      reduce_boolean = lambda k: False

    attrs = list(attrs)  # We're modifying it in place
    for i, (k, v) in enumerate(attrs):
      k = escape.escape_attr_name(k)
      if (v is None or (not v and self.reduce_empty_attributes) or
          reduce_boolean(k)):
        # For our use case, we treat boolean attributes as quoted because they
        # don't require space between them and "/>" in closing tags.
        attrs[i] = k
        last_quoted = i
      else:
        (v, q) = escape.escape_attr_value(
          v, double_quote=not self.remove_optional_attribute_quotes)
        if q == escape.NO_QUOTES:
          attrs[i] = '%s=%s' % (k, v)
          if v[-1] != '/':
            last_no_slash = i
        else:
          q = '"' if q == escape.DOUBLE_QUOTE else "'"
          attrs[i] = '%s=%s%s%s' % (k, q, v, q)
          last_quoted = i

    # 1. If there are no attributes, no additional space is necessary.
    # 2. If last attribute is quoted, no additional space is necessary.
    # 3. Two things are happening here:
    #    a) according to the standard, <foo bar=baz/> should be treated as <foo
    #       bar="baz/"> so space is necessary if this is self-closing tag,
    #       however
    #    b) reportedly (https://github.com/mankyd/htmlmin/pull/12), older
    #       versions of WebKit interpret <foo bar=baz/> as self-closing tag so
    #       we need the space if the last argument ends with a slash.
    space_maybe = ''
    if attrs:
      needs_space = lambda last_attr: (last_attr[-1] not in '"\'' and
                                       (close_tag or last_attr[-1] == '/'))
      if needs_space(attrs[-1][-1]):
        # If moving attributes around can help, do it.  Otherwise bite the
        # bullet and put the space in.
        i = last_no_slash if last_quoted == -1 else last_quoted
        if i == -1 or needs_space(attrs[i]):
          space_maybe = ' '
        else:
          attrs.append(attrs[i])
          del attrs[i]

    return '<%s%s%s%s%s>' % (escape.escape_tag(tag),
                             ' ' if attrs else '',
                             ' '.join(attrs),
                             space_maybe,
                             '/' if close_tag else '')

  def handle_decl(self, decl):
    if (len(self._data_buffer) == 1 and
        whitespace_re.match(self._data_buffer[0])):
      self._data_buffer = []
    self._data_buffer.append('<!' + decl + '>')
    self._after_doctype = True

  def in_tag(self, *tags):
    for t in self._tag_stack:
      if t[0] in tags:
        return t
    return False

  def _close_tags_up_to(self, tag):
    num_pres = 0
    i = 0
    for i, t in enumerate(self._tag_stack):
      if t[1]:
        num_pres += 1
      if t[0] == tag:
        break

      # Only the html tag can close out everything. Put on the brakes if
      # we encounter a closing tag that we didn't recognize.
      if tag != 'html' and t[0] in ('body', 'html', 'head'):
        raise OpenTagNotFoundError()

    self._tag_stack = self._tag_stack[i+1:]

    return num_pres

  def handle_starttag(self, tag, attrs):
    self._after_doctype = False
    if tag == 'head':
      self._in_head = True
    elif self._in_head and tag == 'title':
      self._in_title = True
      self._title_newly_opened = True

    tag_sets = ( # a list of tags and tags that they are closed by
      (('li',), ('li',)),
      (('dd', 'dt',), ('dd', 'dt',)),
      (('rp', 'rt',), ('rp', 'rt',)),
      (('p',), ('address', 'article', 'aside', 'blockquote', 'dir', 'div',
                'dl', 'fieldset', 'footer', 'form', 'h1', 'h2', 'h3', 'h4',
                'h5', 'h6', 'header', 'hgroup', 'hr', 'menu', 'nav', 'ol', 'p',
                'pre', 'section', 'table', 'ul')),
      (('optgroup',), ('optgroup',)),
      (('option',), ('option', 'optgroup')),
      (('colgroup',), ('*',)),
      (('tbody', 'thead',), ('tbody', 'tfoot')),
      (('tfoot',), ('tbody',)),
      (('tr',), ('tr',)),
      (('td', 'th'), ('td', 'th')),
      )
    for open_tags, closed_by_tags in tag_sets:
      in_tag = self.in_tag(*open_tags)
      if in_tag and (tag in closed_by_tags or '*' in closed_by_tags):
        self._in_pre_tag -= self._close_tags_up_to(in_tag[0])

    start_pre = False
    if (tag in self.pre_tags or
        tag in ('script', 'style') or
        self._has_pre(attrs) or
        self._in_pre_tag > 0):
      self._in_pre_tag += 1
      start_pre = True

    self._tag_stack.insert(0, (tag, start_pre))

    if not self.keep_pre:
      attrs = [(k,v) for k,v in attrs if k != self.pre_attr]

    self._data_buffer.append(self.build_tag(tag, attrs, False))

  def handle_endtag(self, tag):
    # According to the spec,
 tags don't get closed when a parent a
    # tag closes them. Here's some logic that addresses this.
    if tag == 'a':
      contains_p = False
      for i, t in enumerate(self._tag_stack):
        if t[0] == 'p':
          contains_p = True
        elif t[0] == 'a':
          break
      if contains_p: # the p tag, and all its children should be left open
        a_tag = self._tag_stack.pop(i)
        if a_tag[1]:
          self._in_pre_tag -= 1
    else:
      if tag == 'head':
        # TODO: Did we know that we were in an head tag?! If not, we need to
        # reminify everything to remove extra spaces.
        self._in_head = False
      elif tag == 'title':
        self._in_title = False
        self._title_newly_opened = False
      try:
        self._in_pre_tag -= self._close_tags_up_to(tag)
      except OpenTagNotFoundError:
        # Some tags don't require a start tag. Most do. Either way, we leave
        # closing tags along since they affect output. For instance, a '
'
        # results in a '

' in Chrome.
        pass
    if tag not in NO_CLOSE_TAGS:
      self._data_buffer.extend(['</', escape.escape_tag(tag), '>'])

  def handle_startendtag(self, tag, attrs):
    self._after_doctype = False
    if not self.keep_pre:
      attrs = [(k,v) for k,v in attrs if k != 'pre']
    self._data_buffer.append(self.build_tag(tag, attrs, tag not in NO_CLOSE_TAGS))

  def handle_comment(self, data):
    if not self.remove_comments or (
        data and (data[0] == '!' or re.match(r'^\[if\s', data))):
      self._data_buffer.append(''.format(
          data[1:] if len(data) and data[0] == '!' else data))

  def handle_data(self, data):
    if self._in_pre_tag > 0:
      self._data_buffer.append(data)
    else:
      # remove_all_empty_space matches everything. remove_empty_space only
      # matches if there's a newline involved.
      if self.remove_all_empty_space or self._in_head or self._after_doctype:
        match = whitespace_re.match(data)
        if match and match.end(0) == len(data):
          return
      elif self.remove_empty_space:
        match = whitespace_newline_re.match(data)
        if match and match.end(0) == len(data):
          return

      # if we're in the title, remove leading and trailing whitespace.
      # note that the title may be parsed in chunks if entityref's or charrefs
      # are encountered.
      if self._in_title:
        if self.__title_trailing_whitespace:
          self._data_buffer.append(' ')
        self.__title_trailing_whitespace = data[-1].isspace()
        if self._title_newly_opened:
          self._title_newly_opened = False
          data = leading_trailing_whitespace_re.sub('', data)
        else:
          data = trailing_whitespace_re.sub(
            '', leading_whitespace_re.sub(' ', data))

      data = whitespace_re.sub(' ', data)
      if not data:
        return

      if self._in_pre_tag == 0 and self._data_buffer:
        # If we're not in a pre block, its possible that we append two spaces
        # together, which we want to avoid. For instance, if we remove a comment
        # from between two blocks of text: a  c => a  c.
        if data[0] == ' ' and self._data_buffer[-1][-1] == ' ':
          data = data[1:]
          if not data:
            return
      self._data_buffer.append(data)

  def handle_entityref(self, data):
    if self._in_title:
      if not self._title_newly_opened and self.__title_trailing_whitespace:
        self._data_buffer.append(' ')
        self.__title_trailing_whitespace = False
      self._title_newly_opened = False
    self._data_buffer.append('&{};'.format(data))

  def handle_charref(self, data):
    if self._in_title:
      if not self._title_newly_opened and self.__title_trailing_whitespace:
        self._data_buffer.append(' ')
        self.__title_trailing_whitespace = False
      self._title_newly_opened = False
    self._data_buffer.append('&#{};'.format(data))

  def handle_pi(self, data):
    self._data_buffer.append('<?' + data + '>')

  def unknown_decl(self, data):
    self._data_buffer.append('<![' + data + ']>')

  def reset(self):
    self._data_buffer = []
    HTMLParser.reset(self)

  @property
  def result(self):
    return ''.join(self._data_buffer)
<|endoftext|>


<commit_before>from ..alpha_vantage.alphavantage import AlphaVantage
from ..alpha_vantage.timeseries import TimeSeries
from ..alpha_vantage.techindicators import TechIndicators
from ..alpha_vantage.sectorperformance import SectorPerformances
from ..alpha_vantage.cryptocurrencies import CryptoCurrencies
from ..alpha_vantage.foreignexchange import ForeignExchange
from pandas import DataFrame as df
import unittest
import mock
import sys
from os import path
import urllib

class TestAlphaVantage(unittest.TestCase):

    _API_KEY_TEST = "test"
    _API_EQ_NAME_TEST = 'MSFT'

    @staticmethod
    def get_file_from_url(url):
        """
            Return the file name used for testing, found in the test data folder
            formed using the orginal url
        """
        tmp = url
        for ch in [':', '/', '.', '?', '=', '&']:
            if ch in tmp:
                tmp = tmp.replace(ch, '_')
        path_dir = path.join(path.dirname(
            path.abspath(__file__)), 'test_data/')
        return path.join(path.join(path_dir, tmp))

    def test_key_none(self):
        """Raise an error when a key has not been given
        """
        try:
            AlphaVantage()
            self.fail(msg='A None api key must raise an error')
        except ValueError:
            self.assertTrue(True)

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_handle_api_call_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        av = AlphaVantage(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data = av._handle_api_call(url)
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 2.7")
    @mock.patch('urllib2.urlopen')
    def test_handle_api_call_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        av = AlphaVantage(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data = av._handle_api_call(url)
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_time_series_intraday_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ts = TimeSeries(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ts.get_intraday(
                "MSFT", interval='1min', outputsize='full')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 3")
    @mock.patch('urllib2.urlopen')
    def test_time_series_intraday_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ts = TimeSeries(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ts.get_intraday(
                "MSFT", interval='1min', outputsize='full')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_technical_indicator_sma_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ti = TechIndicators(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SMA&symbol=MSFT&interval=15min&time_period=10&series_type=close&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ti.get_sma("MSFT", interval='15min',
                                 time_period=10, series_type='close')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 3")
    @mock.patch('urllib2.urlopen')
    def test_technical_indicator_sma_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ti = TechIndicators(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SMA&symbol=MSFT&interval=15min&time_period=10&series_type=close&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ti.get_sma("MSFT", interval='15min',
                                 time_period=10, series_type='close')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_sector_perfomance_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        sp = SectorPerformances(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SECTOR&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = sp.get_sector()
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 3")
    @mock.patch('urllib2.urlopen')
    def test_sector_perfomance_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        sp = SectorPerformances(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SECTOR&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = sp.get_sector()
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_foreign_exchange_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        fe = ForeignExchange(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=BTC&to_currency=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = fe.get_currency_exchange_rate(
                from_currency='BTC', to_currency='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 3")
    @mock.patch('urllib2.urlopen')
    def test_foreign_exchange_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        fe = ForeignExchange(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=BTC&to_currency=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = fe.get_currency_exchange_rate(
                from_currency='BTC', to_currency='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_crypto_currencies_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        cc = CryptoCurrencies(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=DIGITAL_CURRENCY_INTRADAY&symbol=BTC&market=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = cc.get_digital_currency_intraday(
                symbol='BTC', market='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 3")
    @mock.patch('urllib2.urlopen')
    def test_crypto_currencies_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        cc = CryptoCurrencies(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=DIGITAL_CURRENCY_INTRADAY&symbol=BTC&market=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = cc.get_digital_currency_intraday(
                symbol='BTC', market='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')
<commit_msg>Fix typos<commit_after>from ..alpha_vantage.alphavantage import AlphaVantage
from ..alpha_vantage.timeseries import TimeSeries
from ..alpha_vantage.techindicators import TechIndicators
from ..alpha_vantage.sectorperformance import SectorPerformances
from ..alpha_vantage.cryptocurrencies import CryptoCurrencies
from ..alpha_vantage.foreignexchange import ForeignExchange
from pandas import DataFrame as df
import unittest
import mock
import sys
from os import path
import urllib

class TestAlphaVantage(unittest.TestCase):

    _API_KEY_TEST = "test"
    _API_EQ_NAME_TEST = 'MSFT'

    @staticmethod
    def get_file_from_url(url):
        """
            Return the file name used for testing, found in the test data folder
            formed using the orginal url
        """
        tmp = url
        for ch in [':', '/', '.', '?', '=', '&']:
            if ch in tmp:
                tmp = tmp.replace(ch, '_')
        path_dir = path.join(path.dirname(
            path.abspath(__file__)), 'test_data/')
        return path.join(path.join(path_dir, tmp))

    def test_key_none(self):
        """Raise an error when a key has not been given
        """
        try:
            AlphaVantage()
            self.fail(msg='A None api key must raise an error')
        except ValueError:
            self.assertTrue(True)

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_handle_api_call_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        av = AlphaVantage(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data = av._handle_api_call(url)
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 2.7")
    @mock.patch('urllib.urlopen')
    def test_handle_api_call_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        av = AlphaVantage(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data = av._handle_api_call(url)
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_time_series_intraday_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ts = TimeSeries(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ts.get_intraday(
                "MSFT", interval='1min', outputsize='full')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python  2.7")
    @mock.patch('urllib.urlopen')
    def test_time_series_intraday_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ts = TimeSeries(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ts.get_intraday(
                "MSFT", interval='1min', outputsize='full')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_technical_indicator_sma_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ti = TechIndicators(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SMA&symbol=MSFT&interval=15min&time_period=10&series_type=close&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ti.get_sma("MSFT", interval='15min',
                                 time_period=10, series_type='close')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 2.7")
    @mock.patch('urllib.urlopen')
    def test_technical_indicator_sma_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        ti = TechIndicators(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SMA&symbol=MSFT&interval=15min&time_period=10&series_type=close&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = ti.get_sma("MSFT", interval='15min',
                                 time_period=10, series_type='close')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_sector_perfomance_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        sp = SectorPerformances(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SECTOR&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = sp.get_sector()
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python 2.7")
    @mock.patch('urllib.urlopen')
    def test_sector_perfomance_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        sp = SectorPerformances(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=SECTOR&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = sp.get_sector()
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_foreign_exchange_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        fe = ForeignExchange(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=BTC&to_currency=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = fe.get_currency_exchange_rate(
                from_currency='BTC', to_currency='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python  2.7")
    @mock.patch('urllib.urlopen')
    def test_foreign_exchange_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        fe = ForeignExchange(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=BTC&to_currency=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = fe.get_currency_exchange_rate(
                from_currency='BTC', to_currency='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 2, "Test valid for python 3")
    @mock.patch('urllib.request.urlopen')
    def test_crypto_currencies_python3(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        cc = CryptoCurrencies(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=DIGITAL_CURRENCY_INTRADAY&symbol=BTC&market=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = cc.get_digital_currency_intraday(
                symbol='BTC', market='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')

    @unittest.skipIf(sys.version_info.major == 3, "Test valid for python  2.7")
    @mock.patch('urllib.urlopen')
    def test_crypto_currencies_python2(self, mock_urlopen):
        """ Test that api call returns a json file as requested
        """
        cc = CryptoCurrencies(key=TestAlphaVantage._API_KEY_TEST)
        url = "https://www.alphavantage.co/query?function=DIGITAL_CURRENCY_INTRADAY&symbol=BTC&market=CNY&apikey=test"
        path_file = self.get_file_from_url(url)
        with open(path_file) as f:
            mock_urlopen.return_value = f
            data, _ = cc.get_digital_currency_intraday(
                symbol='BTC', market='CNY')
            self.assertIsInstance(
                data, dict, 'Result Data must be a dictionary')
<|endoftext|>


<commit_before>"""
Route groups are classes that allow you to group a set of routes together.

.. currentmodule:: kyoukai.routegroup
"""
import inspect

import collections
import typing

def get_rg_bp(group: 'RouteGroup'):
    """
    Gets the :class:`~.Blueprint` created from a :class:`~.RouteGroup`.
    """
    return getattr(group, "_{0.__name__}__blueprint".format(type(group)))

class RouteGroupType(type):
    """
    The metaclass for a route group.

    This is responsible for passing the keyword arguments to the metaclass.
    """

    def __new__(mcs, name, bases, class_body, **kwargs):
        """
        Override of `__new__` to ensure the __init__ signature is compatible.
        """
        return super().__new__(mcs, name, bases, class_body)

    def __init__(self, name, bases, class_body, **kwargs):
        """
        Override of `__init__` to store the blueprint params.
        """
        super().__init__(name, bases, class_body)
        self._bp_kwargs = kwargs

    def _init_blueprint(self, obb):
        """
        Initializes the Blueprint used by this route group.

        :param obb: The route group instance to intialize.
        """
        # circular imports tm
        from kyoukai.blueprint import Blueprint
        bp = Blueprint(self.__name__, **self._bp_kwargs)
        # get all the method types that have a `.route` attr on them
        for name, value in inspect.getmembers(obb):
            # unwrap methods
            if not hasattr(value, "__func__"):
                continue

            func = value.__func__
            if getattr(func, "in_group", False) is True:
                # check the delegate type
                if func.rg_delegate == "route":
                    # wrap value, but use func attrs
                    # this preserves the method and `self`
                    rtt = bp.wrap_route(value, **func.route_kwargs)

                    # copy hooks
                    for type_, hooks in func.hooks.items():
                        for hook in hooks:
                            rtt.add_hook(type_, hook)

                    bp.add_route(rtt, value.route_url, func.route_methods)
                elif func.rg_delegate == "errorhandler":
                    # add the error handler using `errorhandler_code`
                    bp.add_errorhandler(value, func.errorhandler_code)
                elif func.rg_delegate == "hook":
                    # add the hook
                    bp.add_hook(func.hook_type, value)

        setattr(obb, "_{.__name__}__blueprint".format(self), bp)

    def __call__(self, *args, **kwargs):
        obb = object.__new__(self)
        obb.__init__(*args, **kwargs)
        self._init_blueprint(obb)
        return obb

def route(url: str, methods: typing.Iterable[str] = ("GET",), **kwargs):
    """
    A companion function to the RouteGroup class. This follows :meth:`.Blueprint.route` in
    terms of arguments, and marks a function as a route inside the class.

    This will return the original function, with some attributes attached:

        - ``in_group``: Marks the function as in the route group.
        - ``rg_delegate``: Internal. The type of function inside the group this is.
        - ``route_kwargs``: Keyword arguments to provide to ``wrap_route``.
        - ``route_url``: The routing URL to provide to ``add_route``.
        - ``route_methods``: The methods for the route.
        - ``route_hooks``: A defaultdict of route-specific hooks.

    Additionally, the following methods are added.

        - ``hook``: A decorator that adds a hook of type ``type_``.
        - ``before_request``: A decorator that adds a ``pre`` hook.
        - ``after_request``: A decorator that adds a ``post`` hook.

    .. versionadded:: 2.1.1

    .. versionchanged:: 2.1.3

        Added the ability to add route-specific hooks.

    :param url: The routing URL of the route.
    :param methods: An iterable of methods for the route.
    """

    def inner(func):
        # add the required attrs which are used on a scan later
        func.in_group = True
        func.rg_delegate = "route"
        func.route_kwargs = kwargs
        func.route_url = url
        func.route_methods = methods

        func.route_hooks = collections.defaultdict(lambda: [])

        # helper for route-specific hooks.
        def hook(type_: str):
            def _inner2(hookfunc):
                func.route_hooks[type_].append(hookfunc)
                return hookfunc

            return _inner2

        func.hook = hook
        func.before_request = hook("pre")
        func.after_request = hook("post")

        return func

    return inner

def errorhandler(code: int):
    """
    A companion function to the RouteGroup class. This follows :meth:`.Blueprint.errorhandler` in
    terms of arguments.

    :param code: The code for the error handler.
    """

    def inner(func):
        func.in_group = True
        func.rg_delegate = "errorhandler"
        func.errorhandler_code = code

        return func

    return inner

# hooks

def hook(type_: str):
    """
    Marks a function as a hook.

    :param type_: The type of hook to mark.
    """

    def inner(func):
        func.in_group = True
        func.rg_delegate = "hook"
        func.hook_type = type_

        return func

    return inner

def before_request(func):
    """
    Helper decorator to mark a function as a pre-request hook.
    """
    return hook("pre")(func)

def after_request(func):
    """
    Helper decorator to mark a function as a post-request hook.
    """
    return hook("post")(func)

class RouteGroup(object, metaclass=RouteGroupType):
    """
    A route group is a class that contains multiple methods that are decorated with the route
    decorator. They produce a blueprint that can be added to the tree that includes all methods
    in the route group.

    .. code-block:: python

        class MyGroup(RouteGroup, url_prefix="/api/v1"):
            def __init__(self, something: str):
                self.something = something

            @route("/ping")
            async def ping(self, ctx: HTTPRequestContext):
                return '{"response": self.something}'

    Blueprint parameters can be passed in the class call.

    To add the route group as a blueprint, use
    :meth:`.Blueprint.add_route_group(MyGroup, *args, **kwargs)`.
    """
<commit_msg>HECK<commit_after>"""
Route groups are classes that allow you to group a set of routes together.

.. currentmodule:: kyoukai.routegroup
"""
import inspect

import collections
import typing

def get_rg_bp(group: 'RouteGroup'):
    """
    Gets the :class:`~.Blueprint` created from a :class:`~.RouteGroup`.
    """
    return getattr(group, "_{0.__name__}__blueprint".format(type(group)))

class RouteGroupType(type):
    """
    The metaclass for a route group.

    This is responsible for passing the keyword arguments to the metaclass.
    """

    def __new__(mcs, name, bases, class_body, **kwargs):
        """
        Override of `__new__` to ensure the __init__ signature is compatible.
        """
        return super().__new__(mcs, name, bases, class_body)

    def __init__(self, name, bases, class_body, **kwargs):
        """
        Override of `__init__` to store the blueprint params.
        """
        super().__init__(name, bases, class_body)
        self._bp_kwargs = kwargs

    def _init_blueprint(self, obb):
        """
        Initializes the Blueprint used by this route group.

        :param obb: The route group instance to intialize.
        """
        # circular imports tm
        from kyoukai.blueprint import Blueprint
        bp = Blueprint(self.__name__, **self._bp_kwargs)
        # get all the method types that have a `.route` attr on them
        for name, value in inspect.getmembers(obb):
            # unwrap methods
            if not hasattr(value, "__func__"):
                continue

            func = value.__func__
            if getattr(func, "in_group", False) is True:
                # check the delegate type
                if func.rg_delegate == "route":
                    # wrap value, but use func attrs
                    # this preserves the method and `self`
                    rtt = bp.wrap_route(value, **func.route_kwargs)

                    # copy hooks
                    for type_, hooks in func.route_hooks.items():
                        for hook in hooks:
                            rtt.add_hook(type_, hook)

                    bp.add_route(rtt, value.route_url, func.route_methods)
                elif func.rg_delegate == "errorhandler":
                    # add the error handler using `errorhandler_code`
                    bp.add_errorhandler(value, func.errorhandler_code)
                elif func.rg_delegate == "hook":
                    # add the hook
                    bp.add_hook(func.hook_type, value)

        setattr(obb, "_{.__name__}__blueprint".format(self), bp)

    def __call__(self, *args, **kwargs):
        obb = object.__new__(self)
        obb.__init__(*args, **kwargs)
        self._init_blueprint(obb)
        return obb

def route(url: str, methods: typing.Iterable[str] = ("GET",), **kwargs):
    """
    A companion function to the RouteGroup class. This follows :meth:`.Blueprint.route` in
    terms of arguments, and marks a function as a route inside the class.

    This will return the original function, with some attributes attached:

        - ``in_group``: Marks the function as in the route group.
        - ``rg_delegate``: Internal. The type of function inside the group this is.
        - ``route_kwargs``: Keyword arguments to provide to ``wrap_route``.
        - ``route_url``: The routing URL to provide to ``add_route``.
        - ``route_methods``: The methods for the route.
        - ``route_hooks``: A defaultdict of route-specific hooks.

    Additionally, the following methods are added.

        - ``hook``: A decorator that adds a hook of type ``type_``.
        - ``before_request``: A decorator that adds a ``pre`` hook.
        - ``after_request``: A decorator that adds a ``post`` hook.

    .. versionadded:: 2.1.1

    .. versionchanged:: 2.1.3

        Added the ability to add route-specific hooks.

    :param url: The routing URL of the route.
    :param methods: An iterable of methods for the route.
    """

    def inner(func):
        # add the required attrs which are used on a scan later
        func.in_group = True
        func.rg_delegate = "route"
        func.route_kwargs = kwargs
        func.route_url = url
        func.route_methods = methods

        func.route_hooks = collections.defaultdict(lambda: [])

        # helper for route-specific hooks.
        def hook(type_: str):
            def _inner2(hookfunc):
                func.route_hooks[type_].append(hookfunc)
                return hookfunc

            return _inner2

        func.hook = hook
        func.before_request = hook("pre")
        func.after_request = hook("post")

        return func

    return inner

def errorhandler(code: int):
    """
    A companion function to the RouteGroup class. This follows :meth:`.Blueprint.errorhandler` in
    terms of arguments.

    :param code: The code for the error handler.
    """

    def inner(func):
        func.in_group = True
        func.rg_delegate = "errorhandler"
        func.errorhandler_code = code

        return func

    return inner

# hooks

def hook(type_: str):
    """
    Marks a function as a hook.

    :param type_: The type of hook to mark.
    """

    def inner(func):
        func.in_group = True
        func.rg_delegate = "hook"
        func.hook_type = type_

        return func

    return inner

def before_request(func):
    """
    Helper decorator to mark a function as a pre-request hook.
    """
    return hook("pre")(func)

def after_request(func):
    """
    Helper decorator to mark a function as a post-request hook.
    """
    return hook("post")(func)

class RouteGroup(object, metaclass=RouteGroupType):
    """
    A route group is a class that contains multiple methods that are decorated with the route
    decorator. They produce a blueprint that can be added to the tree that includes all methods
    in the route group.

    .. code-block:: python

        class MyGroup(RouteGroup, url_prefix="/api/v1"):
            def __init__(self, something: str):
                self.something = something

            @route("/ping")
            async def ping(self, ctx: HTTPRequestContext):
                return '{"response": self.something}'

    Blueprint parameters can be passed in the class call.

    To add the route group as a blueprint, use
    :meth:`.Blueprint.add_route_group(MyGroup, *args, **kwargs)`.
    """
<|endoftext|>