├── marcotti ├── models │ ├── __init__.py │ ├── common │ │ ├── __init__.py │ │ ├── events.py │ │ ├── match.py │ │ ├── suppliers.py │ │ ├── enums.py │ │ ├── personnel.py │ │ └── overview.py │ ├── national.py │ └── club.py ├── tools │ ├── __init__.py │ ├── logsetup.py │ ├── testsetup.py │ └── dbsetup.py ├── etl │ ├── ejson │ │ ├── __init__.py │ │ ├── default.py │ │ └── base.py │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ ├── workflows.py │ │ └── transform.py │ └── ecsv │ │ ├── base.py │ │ └── __init__.py ├── version.py ├── __init__.py ├── data │ ├── surfaces.csv │ ├── positions.csv │ ├── templates │ │ ├── logging.skel │ │ ├── loader.skel │ │ ├── test.skel │ │ └── local.skel │ ├── surfaces.json │ ├── positions.json │ ├── countries.csv │ ├── countries-es.csv │ └── timezones.csv └── base.py ├── setup.cfg ├── .gitignore ├── MANIFEST.in ├── requirements.txt ├── tests ├── test_statistics.py ├── test_match.py ├── test_national.py ├── test_events.py ├── test_club.py ├── conftest.py ├── test_personnel.py └── test_overview.py ├── LICENSE ├── setup.py └── README.md /marcotti/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /marcotti/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /marcotti/etl/ejson/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest -------------------------------------------------------------------------------- /marcotti/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.8.0b2' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .cache 3 | __pycache__ 4 | 5 | *.pyc 6 | sandbox/ -------------------------------------------------------------------------------- /marcotti/etl/__init__.py: -------------------------------------------------------------------------------- 1 | from base import ETL, MarcottiLoad, MarcottiTransform 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include data *.csv *.json 4 | -------------------------------------------------------------------------------- /marcotti/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from base import Marcotti, MarcottiConfig 3 | -------------------------------------------------------------------------------- /marcotti/etl/base/__init__.py: -------------------------------------------------------------------------------- 1 | from workflows import ETL 2 | from transform import MarcottiTransform 3 | from load import MarcottiLoad 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic>=0.8.3 2 | coverage>=4.0.1 3 | pytest>=2.8.2 4 | SQLAlchemy>=1.0.9 5 | jinja2>=2.7 6 | clint>=0.4.0 7 | pandas>=0.16.0 8 | -------------------------------------------------------------------------------- /marcotti/etl/ejson/default.py: -------------------------------------------------------------------------------- 1 | from .base import BaseJSON, extract 2 | 3 | 4 | class JSONExtractor(BaseJSON): 5 | pass 6 | 7 | 8 | class JSONStatExtractor(BaseJSON): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/test_statistics.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import pytest 3 | from sqlalchemy.exc import DataError 4 | 5 | import marcotti.models.club as mc 6 | import marcotti.models.common.personnel as mcp 7 | import marcotti.models.common.statistics as mcs 8 | import marcotti.models.common.enums as enums 9 | 10 | -------------------------------------------------------------------------------- /marcotti/tools/logsetup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | import logging.config 5 | 6 | 7 | def setup_logging(settings_path="logging.json", default_level=logging.INFO): 8 | """Setup logging configuration""" 9 | path = settings_path 10 | if os.path.exists(path): 11 | with open(path, 'rt') as f: 12 | config = json.load(f) 13 | logging.config.dictConfig(config) 14 | else: 15 | logging.basicConfig(level=default_level) 16 | -------------------------------------------------------------------------------- /marcotti/data/surfaces.csv: -------------------------------------------------------------------------------- 1 | description,type 2 | Natural Grass,Natural 3 | Celebration bermudagrass,Natural 4 | Tifway 419 bermudagrass,Natural 5 | Platinum TE Pasaplum,Natural 6 | Tifdwarf bermudagrass,Natural 7 | Discovery bermudagrass,Natural 8 | Latitude 36 bermudagrass,Natural 9 | TifGrand bermudagrass,Natural 10 | Artificial Grass,Artificial 11 | AstroTurf Gen 1,Artificial 12 | AstroTurf Gen 2,Artificial 13 | FieldTurf,Artificial 14 | Hybrid Grass,Hybrid 15 | Desso GrassMaster,Hybrid 16 | EZ Hybrid Turf,Hybrid 17 | XtraGrass,Hybrid 18 | -------------------------------------------------------------------------------- /marcotti/data/positions.csv: -------------------------------------------------------------------------------- 1 | name,type 2 | Goalkeeper,Goalkeeper 3 | Defender,Defender 4 | Central Defender,Defender 5 | Libero,Defender 6 | Full-back,Defender 7 | Left Full-back,Defender 8 | Right Full-back,Defender 9 | Wing-back,Defender 10 | Left Wing-back,Defender 11 | Right Wing-back,Defender 12 | Midfielder,Midfielder 13 | Central Midfielder,Midfielder 14 | Holding Midfielder,Midfielder 15 | Deep-lying playmaker,Midfielder 16 | Playmaker,Midfielder 17 | Trequartista,Midfielder 18 | Left Winger,Midfielder 19 | Right Winger,Midfielder 20 | Left Forward,Forward 21 | Central Forward,Forward 22 | Right Forward,Forward 23 | Striker,Forward 24 | Second striker,Forward 25 | False nine,Forward 26 | -------------------------------------------------------------------------------- /marcotti/etl/ejson/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import json 4 | import logging 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def extract(func): 11 | """ 12 | Decorator function. Open and extract data from JSON files. Return list of dictionaries. 13 | 14 | :param func: Wrapped function with *args and **kwargs arguments. 15 | """ 16 | def _wrapper(*args): 17 | out = [] 18 | instance, prefix = args 19 | for fname in glob.glob(os.path.join(getattr(instance, 'directory'), *prefix)): 20 | with open(fname) as g: 21 | out.extend(func(instance, data=json.load(g))) 22 | return out 23 | return _wrapper 24 | 25 | 26 | class BaseJSON(object): 27 | def __init__(self, directory): 28 | self.directory = directory 29 | -------------------------------------------------------------------------------- /marcotti/data/templates/logging.skel: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": false, 4 | "formatters": { 5 | "default": { 6 | "format": "%(asctime)s - %(name)s - %(levelname)s: %(message)s" 7 | }, 8 | "console": { 9 | "format": "%(name)-12s: %(message)s" 10 | } 11 | }, 12 | "handlers": { 13 | "console": { 14 | "class": "logging.StreamHandler", 15 | "level": "INFO", 16 | "formatter": "console", 17 | "stream": "ext://sys.stdout" 18 | }, 19 | "err": { 20 | "class": "logging.StreamHandler", 21 | "level": "INFO", 22 | "formatter": "console", 23 | "stream": "ext://sys.stderr" 24 | }, 25 | "main": { 26 | "class": "logging.handlers.RotatingFileHandler", 27 | "level": "DEBUG", 28 | "formatter": "default", 29 | "filename": "{{ log_file_path }}", 30 | "maxBytes": 20971520, 31 | "backupCount": 6, 32 | "encoding": "utf8" 33 | } 34 | }, 35 | "loggers": { 36 | "": { 37 | "handlers": ["main", "console"], 38 | "level": "DEBUG", 39 | "propagate": true 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2017 Soccermetrics Research, LLC. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /marcotti/data/templates/loader.skel: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from {{ config_file }} import {{ config_class }} 5 | from marcotti import Marcotti 6 | from marcotti.etl.ecsv import CSVExtractor 7 | {% if club_db %} 8 | from marcotti.models.club import ClubSchema 9 | {% else %} 10 | from marcotti.models.national import NatlSchema 11 | {% endif %} 12 | from marcotti.tools.logsetup import setup_logging 13 | from marcotti.etl import ETL, MarcottiTransform, MarcottiEventTransform, MarcottiLoad 14 | 15 | 16 | setup_logging() 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | if __name__ == "__main__": 21 | settings = {{ config_class }}() 22 | marcotti = Marcotti(settings) 23 | with marcotti.create_session() as session: 24 | marcotti.create_db({% if club_db %}ClubSchema{% else %}NatlSchema{% endif %}) 25 | marcotti.initial_load({{ country_prefix }}) 26 | 27 | # Add supplier information to database 28 | 29 | csv = CSVExtractor(settings.CSV_DATA_DIR) 30 | supp_etl = ETL(transform=MarcottiTransform, load=MarcottiLoad, session=session) 31 | supp_etl.workflow('suppliers', csv.suppliers(settings.CSV_DATA['suppliers'])) 32 | 33 | # Load CSV data 34 | 35 | etl = ETL(transform=MarcottiTransform, load=MarcottiLoad, session=session, 36 | supplier=u'{{ supplier }}') 37 | -------------------------------------------------------------------------------- /marcotti/data/surfaces.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Natural Grass", 4 | "type": "Natural" 5 | }, 6 | { 7 | "description": "Celebration bermudagrass", 8 | "type": "Natural" 9 | }, 10 | { 11 | "description": "Tifway 419 bermudagrass", 12 | "type": "Natural" 13 | }, 14 | { 15 | "description": "Platinum TE Pasaplum", 16 | "type": "Natural" 17 | }, 18 | { 19 | "description": "Tifdwarf bermudagrass", 20 | "type": "Natural" 21 | }, 22 | { 23 | "description": "Discovery bermudagrass", 24 | "type": "Natural" 25 | }, 26 | { 27 | "description": "Latitude 36 bermudagrass", 28 | "type": "Natural" 29 | }, 30 | { 31 | "description": "TifGrand bermudagrass", 32 | "type": "Natural" 33 | }, 34 | { 35 | "description": "Artificial Grass", 36 | "type": "Artificial" 37 | }, 38 | { 39 | "description": "AstroTurf Gen 1", 40 | "type": "Artificial" 41 | }, 42 | { 43 | "description": "AstroTurf Gen 2", 44 | "type": "Artificial" 45 | }, 46 | { 47 | "description": "FieldTurf", 48 | "type": "Artificial" 49 | }, 50 | { 51 | "description": "Hybrid Grass", 52 | "type": "Hybrid" 53 | }, 54 | { 55 | "description": "Desso GrassMaster", 56 | "type": "Hybrid" 57 | }, 58 | { 59 | "description": "EZ Hybrid Turf", 60 | "type": "Hybrid" 61 | }, 62 | { 63 | "description": "XtraGrass", 64 | "type": "Hybrid" 65 | } 66 | ] -------------------------------------------------------------------------------- /marcotti/data/templates/test.skel: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.engine import create_engine 3 | from sqlalchemy.orm.session import Session 4 | 5 | from {{ config_file }} import {{ config_class }} 6 | 7 | 8 | class TestConfig({{ config_class }}): 9 | DBNAME = 'test-marcotti-db' 10 | 11 | 12 | @pytest.fixture('session') 13 | def config(): 14 | return TestConfig() 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def db_connection(request, config): 19 | engine = create_engine(config.DATABASE_URI) 20 | connection = engine.connect() 21 | {% if test_schema == 'club' %} 22 | from marcotti.models.club import ClubSchema 23 | schema = ClubSchema 24 | {% elif test_schema == 'natl' %} 25 | from marcotti.models.national import NatlSchema 26 | schema = NatlSchema 27 | {% elif test_schema == 'common' %} 28 | import marcotti.models.common as common 29 | schema = common.BaseSchema 30 | {% endif %} 31 | schema.metadata.create_all(connection) 32 | 33 | def fin(): 34 | schema.metadata.drop_all(connection) 35 | connection.close() 36 | engine.dispose() 37 | request.addfinalizer(fin) 38 | return connection 39 | 40 | 41 | @pytest.fixture() 42 | def session(request, db_connection): 43 | __transaction = db_connection.begin_nested() 44 | session = Session(db_connection) 45 | 46 | def fin(): 47 | session.close() 48 | __transaction.rollback() 49 | request.addfinalizer(fin) 50 | return session 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | 5 | REQUIRES = ['SQLAlchemy>=1.0.9', 6 | 'jinja2>=2.7', 7 | 'clint>=0.4.0'] 8 | needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) 9 | pytest_runner = ['pytest_runner'] if needs_pytest else [] 10 | exec(open('marcotti/version.py').read()) 11 | 12 | setup( 13 | name='marcotti', 14 | version=__version__, 15 | packages=find_packages(), 16 | package_data={ 17 | 'marcotti': ['data/*.csv', 'data/*.json', 'data/templates/*.skel'] 18 | }, 19 | entry_points={ 20 | 'console_scripts': [ 21 | 'dbsetup = marcotti.tools.dbsetup:main', 22 | 'testsetup = marcotti.tools.testsetup:main', 23 | ] 24 | }, 25 | url='https://github.com/soccermetrics/marcotti', 26 | license='MIT', 27 | author='Soccermetrics Research', 28 | author_email='info@soccermetrics.net', 29 | keywords=['soccer', 'football', 'soccer analytics', 'data modeling'], 30 | setup_requires=pytest_runner, 31 | install_requires=REQUIRES, 32 | extras_require={ 33 | 'PostgreSQL': ['psycopg2>=2.5.1'], 34 | 'MySQL': ['mysql-python>=1.2.3'], 35 | 'MSSQL': ['pyodbc>=3.0'], 36 | 'Oracle': ['cx_oracle>=5.0'], 37 | 'Firebird': ['fdb>=1.6'] 38 | }, 39 | tests_require=['pytest>=2.8.2'], 40 | description='Data modeling software library for capture of football match result data', 41 | long_description=open('README.md').read() 42 | ) 43 | -------------------------------------------------------------------------------- /marcotti/tools/testsetup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkg_resources 3 | 4 | import jinja2 5 | from clint.textui import prompt 6 | 7 | 8 | triple_options = [{'selector': '1', 'prompt': 'Club DB', 'return': 'club'}, 9 | {'selector': '2', 'prompt': 'National Team DB', 'return': 'natl'}, 10 | {'selector': '3', 'prompt': 'Common Models', 'return': 'common'}] 11 | 12 | 13 | def setup_user_input(): 14 | config_file = prompt.query('Config file name:', default='local') 15 | config_class = prompt.query('Config class name:', default='LocalConfig') 16 | test_schema = prompt.options('Which tests do you want to run?', triple_options) 17 | _dict = { 18 | 'config_file': config_file.lower(), 19 | 'config_class': config_class, 20 | 'test_schema': test_schema 21 | } 22 | return _dict 23 | 24 | 25 | def main(): 26 | """ 27 | Main test setup function exposed as script command. 28 | """ 29 | DATA_PATH = pkg_resources.resource_filename('marcotti', 'data/') 30 | print("#### Test setup questionnaire ####") 31 | setup_dict = setup_user_input() 32 | print("#### Creating settings and data loader modules ####") 33 | env = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=DATA_PATH), 34 | trim_blocks=True, lstrip_blocks=True) 35 | template_file = 'test.skel' 36 | test_config_file = 'conftest.py' 37 | template = env.get_template(os.path.join('templates', template_file)) 38 | with open(test_config_file, 'w') as g: 39 | result = template.render(setup_dict) 40 | g.write(result) 41 | print("Configured {}".format(test_config_file)) 42 | print("#### Setup complete ####") 43 | -------------------------------------------------------------------------------- /marcotti/data/positions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Goalkeeper", 4 | "type": "Goalkeeper" 5 | }, 6 | { 7 | "name": "Defender", 8 | "type": "Defender" 9 | }, 10 | { 11 | "name": "Central Defender", 12 | "type": "Defender" 13 | }, 14 | { 15 | "name": "Libero", 16 | "type": "Defender" 17 | }, 18 | { 19 | "name": "Full-back", 20 | "type": "Defender" 21 | }, 22 | { 23 | "name": "Left Full-back", 24 | "type": "Defender" 25 | }, 26 | { 27 | "name": "Right Full-back", 28 | "type": "Defender" 29 | }, 30 | { 31 | "name": "Wing-back", 32 | "type": "Defender" 33 | }, 34 | { 35 | "name": "Left Wing-back", 36 | "type": "Defender" 37 | }, 38 | { 39 | "name": "Right Wing-back", 40 | "type": "Defender" 41 | }, 42 | { 43 | "name": "Midfielder", 44 | "type": "Midfielder" 45 | }, 46 | { 47 | "name": "Central Midfielder", 48 | "type": "Midfielder" 49 | }, 50 | { 51 | "name": "Holding Midfielder", 52 | "type": "Midfielder" 53 | }, 54 | { 55 | "name": "Deep-lying playmaker", 56 | "type": "Midfielder" 57 | }, 58 | { 59 | "name": "Playmaker", 60 | "type": "Midfielder" 61 | }, 62 | { 63 | "name": "Trequartista", 64 | "type": "Midfielder" 65 | }, 66 | { 67 | "name": "Left Winger", 68 | "type": "Midfielder" 69 | }, 70 | { 71 | "name": "Right Winger", 72 | "type": "Midfielder" 73 | }, 74 | { 75 | "name": "Left Forward", 76 | "type": "Forward" 77 | }, 78 | { 79 | "name": "Central Forward", 80 | "type": "Forward" 81 | }, 82 | { 83 | "name": "Right Forward", 84 | "type": "Forward" 85 | }, 86 | { 87 | "name": "Striker", 88 | "type": "Forward" 89 | }, 90 | { 91 | "name": "Second striker", 92 | "type": "Forward" 93 | }, 94 | { 95 | "name": "False nine", 96 | "type": "Forward" 97 | } 98 | ] -------------------------------------------------------------------------------- /marcotti/etl/ecsv/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import csv 3 | import glob 4 | import logging 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def extract(func): 11 | """ 12 | Decorator function. Open and extract data from CSV files. Return list of dictionaries. 13 | 14 | :param func: Wrapped function with *args and **kwargs arguments. 15 | """ 16 | def _wrapper(*args): 17 | out = [] 18 | instance, prefix = args 19 | for fname in glob.glob(os.path.join(getattr(instance, 'directory'), *prefix)): 20 | with open(fname) as g: 21 | out.extend(func(instance, data=csv.DictReader(g))) 22 | return out 23 | return _wrapper 24 | 25 | 26 | class BaseCSV(object): 27 | def __init__(self, directory): 28 | self.directory = directory 29 | 30 | @staticmethod 31 | def column(field, **kwargs): 32 | try: 33 | value = kwargs[field].strip() 34 | return value if value != "" else None 35 | except (AttributeError, KeyError, TypeError) as ex: 36 | return None 37 | 38 | def column_unicode(self, field, **kwargs): 39 | try: 40 | return self.column(field, **kwargs).decode('utf-8') 41 | except (KeyError, AttributeError): 42 | return None 43 | 44 | def column_int(self, field, **kwargs): 45 | try: 46 | return int(self.column(field, **kwargs)) 47 | except (KeyError, TypeError): 48 | return None 49 | 50 | def column_bool(self, field, **kwargs): 51 | try: 52 | return bool(self.column_int(field, **kwargs)) 53 | except (KeyError, TypeError): 54 | return None 55 | 56 | def column_float(self, field, **kwargs): 57 | try: 58 | return float(self.column(field, **kwargs)) 59 | except (KeyError, TypeError): 60 | return None 61 | 62 | -------------------------------------------------------------------------------- /marcotti/data/templates/local.skel: -------------------------------------------------------------------------------- 1 | """ 2 | This is a local configuration file for Marcotti. 3 | """ 4 | import os 5 | 6 | from marcotti import MarcottiConfig 7 | 8 | 9 | class {{ config_class }}(MarcottiConfig): 10 | """ 11 | Local Configuration class that contains settings for Marcotti database. 12 | 13 | Multiple configurations can be created by copying or subclassing this class. 14 | """ 15 | # At a minimum, these variables must be defined. 16 | DIALECT = '{{ dialect }}' 17 | DBNAME = '{{ dbname }}' 18 | 19 | # For all other non-SQLite databases, these variables must be set. 20 | DBUSER = '{{ dbuser }}' 21 | DBPASSWD = '' # This should be inserted by user. 22 | HOSTNAME = '{{ dbhost }}' 23 | PORT = {{ dbport }} 24 | 25 | # Define initial start and end years in database. 26 | START_YEAR = {{ start_yr }} 27 | END_YEAR = {{ end_yr }} 28 | 29 | # 30 | # Logging configuration variables 31 | # 32 | 33 | LOG_DIR = r"{{ logging_dir }}" 34 | 35 | # Define CSV data files 36 | CSV_DATA_DIR = r"{{ csv_data_dir }}" 37 | CSV_DATA = { 38 | 'suppliers': {{ csv_data.suppliers }}, 39 | 'competitions': {{ csv_data.competitions }}, 40 | 'seasons': {{ csv_data.seasons }}, 41 | 'clubs': {{ csv_data.clubs }}, 42 | 'venues': {{ csv_data.venues }}, 43 | 'positions': {{ csv_data.positions }}, 44 | 'players': {{ csv_data.players }}, 45 | 'managers': {{ csv_data.managers }}, 46 | 'referees': {{ csv_data.referees }}, 47 | 'league_matches': {{ csv_data.league_matches }}, 48 | 'group_matches': {{ csv_data.group_matches }}, 49 | 'knockout_matches': {{ csv_data.knockout_matches }}, 50 | 'lineups': {{ csv_data.lineups }}, 51 | 'goals': {{ csv_data.goals }}, 52 | 'penalties': {{ csv_data.penalties }}, 53 | 'bookables': {{ csv_data.bookables }}, 54 | 'substitutions': {{ csv_data.substitutions }}, 55 | 'shootouts': {{ csv_data.shootouts }}, 56 | 'statistics': {{ csv_data.stats }} 57 | } 58 | -------------------------------------------------------------------------------- /marcotti/etl/ecsv/__init__.py: -------------------------------------------------------------------------------- 1 | import overview 2 | import personnel 3 | import match 4 | import events 5 | import statistics 6 | 7 | 8 | CSV_ETL_CLASSES = { 9 | 'Supplier': { 10 | 'Supplier': overview.SupplierIngest, 11 | }, 12 | 'Overview': { 13 | 'Competitions': overview.CompetitionIngest, 14 | 'Clubs': overview.ClubIngest, 15 | 'Venues': overview.VenueIngest 16 | }, 17 | 'Personnel': { 18 | 'Positions': personnel.PositionMapIngest, 19 | 'Players': personnel.PlayerIngest, 20 | 'Managers': personnel.ManagerIngest, 21 | 'Referees': personnel.RefereeIngest 22 | }, 23 | 'Match': { 24 | 'Matches': match.MatchIngest, 25 | 'Lineups': match.MatchLineupIngest, 26 | 'Goals': events.GoalIngest, 27 | 'Penalties': events.PenaltyIngest, 28 | 'Bookables': events.BookableIngest, 29 | 'Substitutions': events.SubstitutionIngest, 30 | 'PlayerStats': [ 31 | statistics.AssistsIngest, 32 | statistics.ClearancesIngest, 33 | statistics.CornerCrossesIngest, 34 | statistics.CornersIngest, 35 | statistics.CrossesIngest, 36 | statistics.DefensivesIngest, 37 | statistics.DisciplineIngest, 38 | statistics.DuelsIngest, 39 | statistics.FoulWinsIngest, 40 | statistics.FreeKicksIngest, 41 | statistics.GKActionsIngest, 42 | statistics.GKAllowedGoalsIngest, 43 | statistics.GKAllowedShotsIngest, 44 | statistics.GKSavesIngest, 45 | statistics.GoalBodyPartsIngest, 46 | statistics.GoalLineClearancesIngest, 47 | statistics.GoalLocationsIngest, 48 | statistics.GoalTotalsIngest, 49 | statistics.ImportantPlaysIngest, 50 | statistics.PassDirectionsIngest, 51 | statistics.PassLengthsIngest, 52 | statistics.PassLocationsIngest, 53 | statistics.PassTotalsIngest, 54 | statistics.PenaltyActionsIngest, 55 | statistics.ShotBlocksIngest, 56 | statistics.ShotBodyPartsIngest, 57 | statistics.ShotLocationsIngest, 58 | statistics.ShotPlaysIngest, 59 | statistics.TacklesIngest, 60 | statistics.ThrowinsIngest, 61 | statistics.TouchesIngest, 62 | statistics.TouchLocationsIngest 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /marcotti/models/common/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.types import SchemaType, TypeDecorator, Enum 5 | 6 | BaseSchema = declarative_base(name="Base") 7 | 8 | 9 | class EnumSymbol(object): 10 | """Define a fixed symbol tied to a parent class.""" 11 | 12 | def __init__(self, cls_, name, value, description): 13 | self.cls_ = cls_ 14 | self.name = name 15 | self.value = value 16 | self.description = description 17 | 18 | def __reduce__(self): 19 | """Allow unpickling to return the symbol 20 | linked to the DeclEnum class.""" 21 | return getattr, (self.cls_, self.name) 22 | 23 | def __iter__(self): 24 | return iter([self.value, self.description]) 25 | 26 | def __repr__(self): 27 | return "<%s>" % self.name 28 | 29 | 30 | class EnumMeta(type): 31 | """Generate new DeclEnum classes.""" 32 | 33 | def __init__(cls, classname, bases, dict_): 34 | cls._reg = reg = cls._reg.copy() 35 | for k, v in dict_.items(): 36 | if isinstance(v, tuple): 37 | sym = reg[v[0]] = EnumSymbol(cls, k, *v) 38 | setattr(cls, k, sym) 39 | return type.__init__(cls, classname, bases, dict_) 40 | 41 | def __iter__(cls): 42 | return iter(cls._reg.values()) 43 | 44 | 45 | class DeclEnum(object): 46 | """Declarative enumeration.""" 47 | 48 | __metaclass__ = EnumMeta 49 | _reg = {} 50 | 51 | @classmethod 52 | def from_string(cls, value): 53 | try: 54 | return cls._reg[value] 55 | except KeyError: 56 | raise ValueError( 57 | "Invalid value for %r: %r" % 58 | (cls.__name__, value) 59 | ) 60 | 61 | @classmethod 62 | def values(cls): 63 | return cls._reg.keys() 64 | 65 | @classmethod 66 | def db_type(cls): 67 | return DeclEnumType(cls) 68 | 69 | 70 | class DeclEnumType(SchemaType, TypeDecorator): 71 | def __init__(self, enum): 72 | self.enum = enum 73 | self.impl = Enum( 74 | *enum.values(), 75 | name="ck%s" % re.sub( 76 | '([A-Z])', 77 | lambda m:"_" + m.group(1).lower(), 78 | enum.__name__) 79 | ) 80 | 81 | def _set_table(self, table, column): 82 | self.impl._set_table(table, column) 83 | 84 | def copy(self): 85 | return DeclEnumType(self.enum) 86 | 87 | def process_bind_param(self, value, dialect): 88 | if value is None: 89 | return None 90 | return value.value 91 | 92 | def process_result_value(self, value, dialect): 93 | if value is None: 94 | return None 95 | return self.enum.from_string(value.strip()) 96 | -------------------------------------------------------------------------------- /marcotti/models/common/events.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Sequence, ForeignKey 2 | from sqlalchemy.schema import CheckConstraint 3 | from sqlalchemy.orm import relationship, backref 4 | 5 | from marcotti.models.common import BaseSchema 6 | import marcotti.models.common.enums as enums 7 | 8 | 9 | class MatchTimeMixin(object): 10 | time = Column(Integer, CheckConstraint('time > 0 AND time <= 120'), nullable=False) 11 | stoppage = Column(Integer, CheckConstraint('stoppage >= 0 AND stoppage < 20'), default=0) 12 | 13 | 14 | class Goals(MatchTimeMixin, BaseSchema): 15 | __tablename__ = "goals" 16 | 17 | id = Column(Integer, Sequence('goal_id_seq', start=100000), primary_key=True) 18 | 19 | domain = Column(String) 20 | bodypart = Column(enums.BodypartType.db_type()) 21 | event = Column(enums.ShotEventType.db_type()) 22 | 23 | lineup_id = Column(Integer, ForeignKey('lineups.id')) 24 | lineup = relationship('MatchLineups', backref=backref('goals')) 25 | 26 | __mapper_args__ = { 27 | 'polymorphic_identity': 'goals', 28 | 'polymorphic_on': domain 29 | } 30 | 31 | 32 | class Penalties(MatchTimeMixin, BaseSchema): 33 | __tablename__ = "penalties" 34 | 35 | id = Column(Integer, Sequence('penalty_id_seq', start=100000), primary_key=True) 36 | 37 | foul = Column(enums.FoulEventType.db_type()) 38 | outcome = Column(enums.ShotOutcomeType.db_type()) 39 | 40 | lineup_id = Column(Integer, ForeignKey('lineups.id')) 41 | lineup = relationship('MatchLineups', backref=backref('penalties')) 42 | 43 | 44 | class Bookables(MatchTimeMixin, BaseSchema): 45 | __tablename__ = "bookable_offenses" 46 | 47 | id = Column(Integer, Sequence('bookable_id_seq', start=100000), primary_key=True) 48 | 49 | foul = Column(enums.FoulEventType.db_type()) 50 | card = Column(enums.CardType.db_type()) 51 | 52 | lineup_id = Column(Integer, ForeignKey('lineups.id')) 53 | lineup = relationship('MatchLineups', backref=backref('bookables')) 54 | 55 | 56 | class Substitutions(MatchTimeMixin, BaseSchema): 57 | __tablename__ = "substitutions" 58 | 59 | id = Column(Integer, Sequence('substitution_id_seq', start=100000), primary_key=True) 60 | 61 | lineup_in_id = Column(Integer, ForeignKey('lineups.id'), nullable=True) 62 | lineup_out_id = Column(Integer, ForeignKey('lineups.id')) 63 | 64 | lineup_in = relationship('MatchLineups', foreign_keys=[lineup_in_id], backref=backref('subbed_in')) 65 | lineup_out = relationship('MatchLineups', foreign_keys=[lineup_out_id], backref=backref('subbed_out')) 66 | 67 | 68 | class PenaltyShootouts(BaseSchema): 69 | __tablename__ = "penalty_shootouts" 70 | 71 | id = Column(Integer, Sequence('shootout_id_seq', start=100000), primary_key=True) 72 | 73 | round = Column(Integer) 74 | num = Column(Integer) 75 | outcome = Column(enums.ShotOutcomeType.db_type()) 76 | 77 | lineup_id = Column(Integer, ForeignKey('lineups.id')) 78 | lineup = relationship('MatchLineups', backref=backref('shootouts')) 79 | -------------------------------------------------------------------------------- /marcotti/base.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import logging 4 | import pkg_resources 5 | from contextlib import contextmanager 6 | 7 | from sqlalchemy.engine import create_engine 8 | from sqlalchemy.orm.session import Session 9 | 10 | from .version import __version__ 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class Marcotti(object): 17 | 18 | def __init__(self, config): 19 | logger.info("Marcotti v{0}: Python {1} on {2}".format( 20 | __version__, sys.version, sys.platform)) 21 | logger.info("Opened connection to {0}".format(self._public_db_uri(config.database_uri))) 22 | self.engine = create_engine(config.database_uri) 23 | self.connection = self.engine.connect() 24 | 25 | @staticmethod 26 | def _public_db_uri(uri): 27 | """ 28 | Strip out database username/password from database URI. 29 | 30 | :param uri: Database URI string. 31 | :return: Database URI with username/password removed. 32 | """ 33 | return re.sub(r"//.*@", "//", uri) 34 | 35 | def create_db(self, base): 36 | """ 37 | Create database models from database schema object. 38 | 39 | :param base: Base schema object that contains data model objects. 40 | """ 41 | logger.info("Creating data models") 42 | base.metadata.create_all(self.connection) 43 | 44 | @contextmanager 45 | def create_session(self): 46 | """ 47 | Open transaction session with an active database object. 48 | 49 | If an error occurs during the session, roll back uncommitted changes 50 | and report error to log file and user. 51 | 52 | If session is no longer needed, commit remaining transactions before closing it. 53 | """ 54 | session = Session(self.connection) 55 | logger.info("Create session {0} with {1}".format( 56 | id(session), self._public_db_uri(str(self.engine.url)))) 57 | try: 58 | yield session 59 | session.commit() 60 | logger.info("Committing remaining transactions to database") 61 | except Exception as ex: 62 | session.rollback() 63 | logger.exception("Database transactions rolled back") 64 | raise ex 65 | finally: 66 | logger.info("Session {0} with {1} closed".format( 67 | id(session), self._public_db_uri(str(self.engine.url)))) 68 | session.close() 69 | 70 | 71 | class MarcottiConfig(object): 72 | """ 73 | Base configuration class for Marcotti. Contains one method that defines the database URI. 74 | 75 | This class is to be subclassed and its attributes defined therein. 76 | """ 77 | 78 | @property 79 | def database_uri(self): 80 | if getattr(self, 'DIALECT') == 'sqlite': 81 | uri = r'sqlite://{p.DBNAME}'.format(p=self) 82 | else: 83 | uri = r'{p.DIALECT}://{p.DBUSER}:{p.DBPASSWD}@{p.HOSTNAME}:{p.PORT}/{p.DBNAME}'.format(p=self) 84 | return uri 85 | -------------------------------------------------------------------------------- /marcotti/etl/base/workflows.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import pandas as pd 4 | from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound 5 | 6 | from marcotti.models.common.suppliers import Suppliers 7 | 8 | 9 | class ETL(object): 10 | """ 11 | Top-level ETL workflow. 12 | 13 | Receive extracted data from XML and/or CSV sources, transform/validate it, and load it to database. 14 | """ 15 | 16 | def __init__(self, **kwargs): 17 | self.supplier = kwargs.get('supplier') 18 | self.transformer = kwargs.get('transform')(kwargs.get('session'), self.supplier) 19 | self.loader = kwargs.get('load')(kwargs.get('session'), self.supplier) 20 | 21 | def workflow(self, entity, *data): 22 | """ 23 | Implement ETL workflow for a specific data entity: 24 | 25 | 1. Combine data extracted from data sources. 26 | 2. Transform and validate combined data into IDs and enums in the Marcotti database. 27 | 3. Load transformed data into the database if it is not already there. 28 | 29 | :param entity: Data model name 30 | :param data: Data payloads from XML and/or CSV sources, in lists of dictionaries 31 | """ 32 | getattr(self.loader, entity)(getattr(self.transformer, entity)(self.combiner(*data))) 33 | 34 | @staticmethod 35 | def combiner(*data_dicts): 36 | """ 37 | Combine data from primary and supplemental data sources using unique ID of primary records. 38 | 39 | Returns a Pandas DataFrame of the combined data. 40 | 41 | :param data_dicts: List of data payloads from data sources, primary source first in list. 42 | :return: DataFrame of combined data. 43 | """ 44 | data_frames = [pd.DataFrame(data) for data in data_dicts] 45 | if len(data_frames) > 1: 46 | new_frames = [data_frame.dropna(axis=1, how='all') for data_frame in data_frames] 47 | return pd.merge(*new_frames, on=['remote_id']) 48 | return data_frames[0] 49 | 50 | 51 | class WorkflowBase(object): 52 | 53 | def __init__(self, session, supplier): 54 | self.session = session 55 | self.supplier_id = self.get_id(Suppliers, name=supplier) if supplier else None 56 | 57 | def get_id(self, model, **conditions): 58 | try: 59 | record_id = self.session.query(model).filter_by(**conditions).one().id 60 | except NoResultFound as ex: 61 | print "{} has no records in Marcotti database for: {}".format(model.__name__, conditions) 62 | return None 63 | except MultipleResultsFound as ex: 64 | print "{} has multiple records in Marcotti database for: {}".format(model.__name__, conditions) 65 | return None 66 | return record_id 67 | 68 | @staticmethod 69 | def make_date_object(iso_date): 70 | """ 71 | Convert ISO date string into datetime.date object. 72 | 73 | :param iso_date: Date string in ISO 8601 format. 74 | :return: :class:`datetime.date` object. 75 | """ 76 | try: 77 | yr, mo, da = [int(x) for x in iso_date.split('-')] 78 | return date(yr, mo, da) 79 | except ValueError: 80 | return None 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Marcotti 2 | ======== 3 | 4 | Marcotti (formerly named the Football Match Result Database) is a data schema that captures match result data in 5 | order to support football research activities. Match result data is defined as historical and publicly-available 6 | data that includes the following: 7 | 8 | * Friendly matches and matches that make up league, knockout or hybrid (group + knockout) football competitions, and 9 | involve either clubs or national team selections. 10 | * Complete data on participating personnel such as players, managers, and match officials. 11 | * Complete top-level data on the football match, including match date, competition name, participating teams, venues, 12 | and environmental conditions. 13 | * Complete data on macro-events that occur during a match, including goals, penalties, disciplinary events, 14 | substitutions, and penalty shootouts. 15 | * Summary in-match statistics on participating players, including crossing, passing, shooting, defensive, 16 | disciplinary, and goalkeeping categories. 17 | 18 | The Marcotti data schema is made up of backend-independent SQLAlchemy objects, and club and national team databases are 19 | built from these objects. 20 | 21 | ## Installation 22 | 23 | Marcotti is written in Python and uses the SQLAlchemy package heavily. 24 | 25 | While not required, [virtualenv](https://pypi.python.org/pypi/virtualenv) is strongly recommended and 26 | [virtualenvwrapper](https://pypi.python.org/pypi/virtualenvwrapper) is very convenient. 27 | 28 | Installation instructions: 29 | 30 | 1. Setup the virtual environment and use `pip` to install the package: 31 | 32 | $ cd /path/to/working/dir 33 | $ mkvirtualenv marcotti 34 | (marcotti) $ pip install git+https://github.com/soccermetrics/marcotti.git[@{release_tag}] 35 | 36 | 2. Run the `dbsetup` command and answer the setup questions to create configuration and data loading scripts. 37 | 38 | ```shell 39 | (marcotti-mls) $ dbsetup 40 | #### Please answer the following questions to setup the folder #### 41 | Work folder (must exist): [.] /path/to/files 42 | Logging folder (must exist): [.] /path/to/logs 43 | Config file name: [local] 44 | Config class name: [LocalConfig] 45 | ``` 46 | The command will produce three files in the working folder: 47 | 48 | * `local.py`: A user-defined database configuration file 49 | * `logging.json`: Default logging configuration file 50 | * `loader.py`: Data loading module 51 | 52 | ## Data Models 53 | 54 | Two data schemas are created - one for clubs, the other for national teams. There is a collection of common data 55 | models upon which both schemas are based, and data models specific to either schema. 56 | 57 | The common data models are classified into five categories: 58 | 59 | * **Overview**: High-level data about the football competition 60 | * **Personnel**: Participants and officials in the football match 61 | * **Match**: High-level data about the match 62 | * **Match Events**: The main events of the football match 63 | * **Statistics**: Summary statistics of participating players in the football match 64 | * **Suppliers**: Mapping data records from outside sources to Marcotti database 65 | 66 | ## Documentation 67 | 68 | The [Marcotti wiki](https://github.com/soccermetrics/marcotti/wiki) contains extensive user documentation of the 69 | package. 70 | 71 | ## License 72 | 73 | (c) 2015-2017 Soccermetrics Research, LLC. Created under MIT license. See `LICENSE` file for details. 74 | -------------------------------------------------------------------------------- /marcotti/models/common/match.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Column, Integer, Numeric, Date, Time, 2 | String, Sequence, ForeignKey, Boolean) 3 | from sqlalchemy.schema import CheckConstraint 4 | from sqlalchemy.orm import relationship, backref 5 | from sqlalchemy.ext.hybrid import hybrid_property 6 | 7 | 8 | from models.common import BaseSchema 9 | import models.common.enums as enums 10 | 11 | 12 | class Matches(BaseSchema): 13 | """ 14 | Matches common data model. 15 | """ 16 | __tablename__ = "matches" 17 | 18 | id = Column(Integer, Sequence('match_id_seq', start=1000000), primary_key=True) 19 | 20 | date = Column(Date) 21 | first_half_length = Column(Integer, CheckConstraint('first_half_length > 0'), default=45) 22 | second_half_length = Column(Integer, CheckConstraint('second_half_length >= 0'), default=45) 23 | first_extra_length = Column(Integer, CheckConstraint('first_extra_length >= 0'), default=0) 24 | second_extra_length = Column(Integer, CheckConstraint('second_extra_length >= 0'), default=0) 25 | attendance = Column(Integer, CheckConstraint('attendance >= 0'), default=0) 26 | phase = Column(String) 27 | 28 | competition_id = Column(Integer, ForeignKey('competitions.id')) 29 | season_id = Column(Integer, ForeignKey('seasons.id')) 30 | venue_id = Column(Integer, ForeignKey('venues.id')) 31 | referee_id = Column(Integer, ForeignKey('referees.id')) 32 | home_manager_id = Column(Integer, ForeignKey('managers.id')) 33 | away_manager_id = Column(Integer, ForeignKey('managers.id')) 34 | 35 | competition = relationship('Competitions', backref=backref('matches', lazy='dynamic')) 36 | season = relationship('Seasons', backref=backref('matches')) 37 | venue = relationship('Venues', backref=backref('matches', lazy='dynamic')) 38 | referee = relationship('Referees', backref=backref('matches')) 39 | home_manager = relationship('Managers', foreign_keys=[home_manager_id], backref=backref('home_matches')) 40 | away_manager = relationship('Managers', foreign_keys=[away_manager_id], backref=backref('away_matches')) 41 | 42 | __mapper_args__ = { 43 | 'polymorphic_identity': 'matches', 44 | 'polymorphic_on': phase 45 | } 46 | 47 | 48 | class MatchConditions(BaseSchema): 49 | __tablename__ = 'match_conditions' 50 | 51 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 52 | 53 | kickoff_time = Column(Time) 54 | kickoff_temp = Column(Numeric(3, 1), CheckConstraint('kickoff_temp >= -15.0 AND kickoff_temp <= 50.0')) 55 | kickoff_humidity = Column(Numeric(4, 1), CheckConstraint('kickoff_humidity >= 0.0 AND kickoff_humidity <= 100.0')) 56 | kickoff_weather = Column(enums.WeatherConditionType.db_type()) 57 | halftime_weather = Column(enums.WeatherConditionType.db_type()) 58 | fulltime_weather = Column(enums.WeatherConditionType.db_type()) 59 | 60 | match = relationship('Matches', backref=backref('conditions')) 61 | 62 | def __repr__(self): 63 | return "".format( 64 | self.id, self.kickoff_time.strftime("%H:%M"), self.kickoff_temp, self.kickoff_humidity, 65 | self.kickoff_weather.value 66 | ) 67 | 68 | 69 | class LeagueMatches(BaseSchema): 70 | """ 71 | League Matches data model. 72 | """ 73 | __abstract__ = True 74 | 75 | matchday = Column(Integer) 76 | 77 | 78 | class GroupMatches(BaseSchema): 79 | """ 80 | Group Matches data model. 81 | """ 82 | __abstract__ = True 83 | 84 | matchday = Column(Integer) 85 | group = Column(String(length=2)) 86 | group_round = Column(enums.GroupRoundType.db_type()) 87 | 88 | 89 | class KnockoutMatches(BaseSchema): 90 | """ 91 | Knockout Matches data model. 92 | """ 93 | __abstract__ = True 94 | 95 | matchday = Column(Integer, default=1) 96 | extra_time = Column(Boolean, default=False) 97 | ko_round = Column(enums.KnockoutRoundType.db_type()) 98 | 99 | 100 | class MatchLineups(BaseSchema): 101 | """ 102 | Match Lineups common data model. 103 | """ 104 | __tablename__ = 'lineups' 105 | 106 | id = Column(Integer, Sequence('lineup_id_seq', start=1000000), primary_key=True) 107 | 108 | is_starting = Column(Boolean, default=False) 109 | is_captain = Column(Boolean, default=False) 110 | type = Column(String) 111 | 112 | match_id = Column(Integer, ForeignKey('matches.id')) 113 | player_id = Column(Integer, ForeignKey('players.id')) 114 | position_id = Column(Integer, ForeignKey('positions.id')) 115 | 116 | match = relationship('Matches', backref=backref('lineups')) 117 | player = relationship('Players', backref=backref('lineups')) 118 | position = relationship('Positions') 119 | 120 | __mapper_args__ = { 121 | 'polymorphic_identity': 'lineups', 122 | 'polymorphic_on': type 123 | } 124 | 125 | @hybrid_property 126 | def full_name(self): 127 | return self.player.full_name 128 | -------------------------------------------------------------------------------- /marcotti/data/countries.csv: -------------------------------------------------------------------------------- 1 | name,code,confederation 2 | Afghanistan,AFG,AFC 3 | Australia,AUS,AFC 4 | Bahrain,BHR,AFC 5 | Bangladesh,BAN,AFC 6 | Bhutan,BHU,AFC 7 | Brunei Darussalam,BRU,AFC 8 | Cambodia,CAM,AFC 9 | China PR,CHN,AFC 10 | Chinese Taipei,TPE,AFC 11 | Guam,GUM,AFC 12 | Hong Kong,HKG,AFC 13 | India,IND,AFC 14 | Indonesia,IDN,AFC 15 | Iran,IRN,AFC 16 | Iraq,IRQ,AFC 17 | Japan,JPN,AFC 18 | Jordan,JOR,AFC 19 | Korea DPR,PRK,AFC 20 | Korea Republic,KOR,AFC 21 | Kuwait,KUW,AFC 22 | Kyrgyzstan,KGZ,AFC 23 | Laos,LAO,AFC 24 | Lebanon,LIB,AFC 25 | Macau,MAC,AFC 26 | Malaysia,MAS,AFC 27 | Maldives,MDV,AFC 28 | Mongolia,MNG,AFC 29 | Myanmar,MYA,AFC 30 | Nepal,NEP,AFC 31 | Northern Mariana Islands,NMI,AFC 32 | Oman,OMA,AFC 33 | Pakistan,PAK,AFC 34 | Palestine,PLE,AFC 35 | Philippines,PHI,AFC 36 | Qatar,QAT,AFC 37 | Saudi Arabia,KSA,AFC 38 | Singapore,SIN,AFC 39 | Sri Lanka,SRI,AFC 40 | Syria,SYR,AFC 41 | Tajikistan,TJK,AFC 42 | Thailand,THA,AFC 43 | Timor-Leste,TLS,AFC 44 | Turkmenistan,TKM,AFC 45 | United Arab Emirates,UAE,AFC 46 | Uzbekistan,UZB,AFC 47 | Vietnam,VIE,AFC 48 | Yemen,YEM,AFC 49 | Algeria,ALG,CAF 50 | Angola,ANG,CAF 51 | Benin,BEN,CAF 52 | Botswana,BOT,CAF 53 | Burkina Faso,BFA,CAF 54 | Burundi,BDI,CAF 55 | Cameroon,CMR,CAF 56 | Cape Verde,CPV,CAF 57 | Central African Republic,CTA,CAF 58 | Chad,CHA,CAF 59 | Comoros,COM,CAF 60 | Congo,CGO,CAF 61 | Congo DR,COD,CAF 62 | Côte d'Ivoire,CIV,CAF 63 | Djibouti,DJI,CAF 64 | Egypt,EGY,CAF 65 | Equatorial Guinea,EQG,CAF 66 | Eritrea,ERI,CAF 67 | Ethiopia,ETH,CAF 68 | Gabon,GAB,CAF 69 | Gambia,GAM,CAF 70 | Ghana,GHA,CAF 71 | Guinea,GUI,CAF 72 | Guinea-Bissau,GNB,CAF 73 | Kenya,KEN,CAF 74 | Lesotho,LES,CAF 75 | Liberia,LBR,CAF 76 | Libya,LBY,CAF 77 | Madagascar,MAD,CAF 78 | Malawi,MWI,CAF 79 | Mali,MLI,CAF 80 | Mauritania,MTN,CAF 81 | Mauritius,MRI,CAF 82 | Morocco,MAR,CAF 83 | Mozambique,MOZ,CAF 84 | Namibia,NAM,CAF 85 | Niger,NIG,CAF 86 | Nigeria,NGA,CAF 87 | Réunion,REU,CAF 88 | Rwanda,RWA,CAF 89 | São Tomé and Príncipe,STP,CAF 90 | Senegal,SEN,CAF 91 | Seychelles,SEY,CAF 92 | Sierra Leone,SLE,CAF 93 | Somalia,SOM,CAF 94 | South Africa,RSA,CAF 95 | South Sudan,SSD,CAF 96 | Sudan,SDN,CAF 97 | Swaziland,SWZ,CAF 98 | Tanzania,TAN,CAF 99 | Togo,TOG,CAF 100 | Tunisia,TUN,CAF 101 | Uganda,UGA,CAF 102 | Zambia,ZAM,CAF 103 | Zimbabwe,ZIM,CAF 104 | Anguilla,AIA,CONCACAF 105 | Antigua and Barbuda,ATG,CONCACAF 106 | Aruba,ARU,CONCACAF 107 | Bahamas,BAH,CONCACAF 108 | Barbados,BRB,CONCACAF 109 | Belize,BLZ,CONCACAF 110 | Bermuda,BER,CONCACAF 111 | British Virgin Islands,VGB,CONCACAF 112 | Canada,CAN,CONCACAF 113 | Cayman Islands,CAY,CONCACAF 114 | Costa Rica,CRC,CONCACAF 115 | Cuba,CUB,CONCACAF 116 | Curaçao,CUW,CONCACAF 117 | Dominica,DMA,CONCACAF 118 | Dominican Republic,DOM,CONCACAF 119 | El Salvador,SLV,CONCACAF 120 | French Guiana,GYF,CONCACAF 121 | Grenada,GRN,CONCACAF 122 | Guadeloupe,GPE,CONCACAF 123 | Guatemala,GUA,CONCACAF 124 | Guyana,GUY,CONCACAF 125 | Haiti,HAI,CONCACAF 126 | Honduras,HON,CONCACAF 127 | Jamaica,JAM,CONCACAF 128 | Martinique,MTQ,CONCACAF 129 | Mexico,MEX,CONCACAF 130 | Montserrat,MSR,CONCACAF 131 | Netherlands Antilles,ANT,CONCACAF 132 | Nicaragua,NCA,CONCACAF 133 | Panama,PAN,CONCACAF 134 | Puerto Rico,PUR,CONCACAF 135 | Saint Kitts and Nevis,SKN,CONCACAF 136 | Saint Lucia,LCA,CONCACAF 137 | Saint Vincent and the Grenadines,VIN,CONCACAF 138 | Saint-Martin,SMT,CONCACAF 139 | Sint Maarten,SXM,CONCACAF 140 | Suriname,SUR,CONCACAF 141 | Trinidad and Tobago,TRI,CONCACAF 142 | Turks and Caicos Islands,TCA,CONCACAF 143 | U.S. Virgin Islands,VIR,CONCACAF 144 | USA,USA,CONCACAF 145 | Argentina,ARG,CONMEBOL 146 | Bolivia,BOL,CONMEBOL 147 | Brazil,BRA,CONMEBOL 148 | Chile,CHI,CONMEBOL 149 | Colombia,COL,CONMEBOL 150 | Ecuador,ECU,CONMEBOL 151 | Paraguay,PAR,CONMEBOL 152 | Peru,PER,CONMEBOL 153 | Uruguay,URU,CONMEBOL 154 | Venezuela,VEN,CONMEBOL 155 | American Samoa,ASA,OFC 156 | Cook Islands,COK,OFC 157 | Federated States of Micronesia,FSM,OFC 158 | Fiji,FIJ,OFC 159 | Kiribati,KIR,OFC 160 | New Caledonia,NCL,OFC 161 | New Zealand,NZL,OFC 162 | Niue,NIU,OFC 163 | Palau,PLW,OFC 164 | Papua New Guinea,PNG,OFC 165 | Samoa,SAM,OFC 166 | Solomon Islands,SOL,OFC 167 | Tahiti,TAH,OFC 168 | Tonga,TGA,OFC 169 | Tuvalu,TUV,OFC 170 | Vanuatu,VAN,OFC 171 | Albania,ALB,UEFA 172 | Andorra,AND,UEFA 173 | Armenia,ARM,UEFA 174 | Austria,AUT,UEFA 175 | Azerbaijan,AZE,UEFA 176 | Belarus,BLR,UEFA 177 | Belgium,BEL,UEFA 178 | Bosnia and Herzegovina,BIH,UEFA 179 | Bulgaria,BUL,UEFA 180 | Croatia,CRO,UEFA 181 | Cyprus,CYP,UEFA 182 | Czech Republic,CZE,UEFA 183 | Denmark,DEN,UEFA 184 | England,ENG,UEFA 185 | Estonia,EST,UEFA 186 | Faroe Islands,FRO,UEFA 187 | Finland,FIN,UEFA 188 | France,FRA,UEFA 189 | FYR Macedonia,MKD,UEFA 190 | Georgia,GEO,UEFA 191 | Germany,GER,UEFA 192 | Gibraltar,GIB,UEFA 193 | Greece,GRE,UEFA 194 | Hungary,HUN,UEFA 195 | Iceland,ISL,UEFA 196 | Israel,ISR,UEFA 197 | Italy,ITA,UEFA 198 | Kazakhstan,KAZ,UEFA 199 | Kosovo,KVX,UEFA 200 | Latvia,LVA,UEFA 201 | Liechtenstein,LIE,UEFA 202 | Lithuania,LTU,UEFA 203 | Luxembourg,LUX,UEFA 204 | Malta,MLT,UEFA 205 | Moldova,MDA,UEFA 206 | Montenegro,MNE,UEFA 207 | Netherlands,NED,UEFA 208 | Northern Ireland,NIR,UEFA 209 | Norway,NOR,UEFA 210 | Poland,POL,UEFA 211 | Portugal,POR,UEFA 212 | Republic of Ireland,IRL,UEFA 213 | Romania,ROU,UEFA 214 | Russia,RUS,UEFA 215 | San Marino,SMR,UEFA 216 | Scotland,SCO,UEFA 217 | Serbia,SRB,UEFA 218 | Slovakia,SVK,UEFA 219 | Slovenia,SVN,UEFA 220 | Spain,ESP,UEFA 221 | Sweden,SWE,UEFA 222 | Switzerland,SUI,UEFA 223 | Turkey,TUR,UEFA 224 | Ukraine,UKR,UEFA 225 | Wales,WAL,UEFA 226 | -------------------------------------------------------------------------------- /marcotti/data/countries-es.csv: -------------------------------------------------------------------------------- 1 | name,code,confederation 2 | Afganistán,AFG,AFC 3 | Australia,AUS,AFC 4 | Baréin,BHR,AFC 5 | Bangladés,BAN,AFC 6 | Bután,BHU,AFC 7 | Brunéi,BRU,AFC 8 | Camboya,CAM,AFC 9 | China,CHN,AFC 10 | Taiwán,TPE,AFC 11 | Guam,GUM,AFC 12 | Hong Kong,HKG,AFC 13 | India,IND,AFC 14 | Indonesia,IDN,AFC 15 | Irán,IRN,AFC 16 | Irak,IRQ,AFC 17 | Japón,JPN,AFC 18 | Jordania,JOR,AFC 19 | Corea del Norte,PRK,AFC 20 | Corea del Sur,KOR,AFC 21 | Kuwait,KUW,AFC 22 | Kirguistán,KGZ,AFC 23 | Laos,LAO,AFC 24 | Líbano,LIB,AFC 25 | Macao,MAC,AFC 26 | Malasia,MAS,AFC 27 | Maldivas,MDV,AFC 28 | Mongolia,MNG,AFC 29 | Birmania,MYA,AFC 30 | Nepal,NEP,AFC 31 | Islas Marianas del Norte,NMI,AFC 32 | Omán,OMA,AFC 33 | Pakistán,PAK,AFC 34 | Palestina,PLE,AFC 35 | Filipinas,PHI,AFC 36 | Catar,QAT,AFC 37 | Arabia Saudita,KSA,AFC 38 | Singapur,SIN,AFC 39 | Sri Lanka,SRI,AFC 40 | Siria,SYR,AFC 41 | Tayikistán,TJK,AFC 42 | Tailandia,THA,AFC 43 | Timor Oriental,TLS,AFC 44 | Turkmenistán,TKM,AFC 45 | Emiratos Árabes Unidos,UAE,AFC 46 | Uzbekistán,UZB,AFC 47 | Vietnam,VIE,AFC 48 | Yemen,YEM,AFC 49 | Argelia,ALG,CAF 50 | Angola,ANG,CAF 51 | Benín,BEN,CAF 52 | Botsuana,BOT,CAF 53 | Burkina Faso,BFA,CAF 54 | Burundi,BDI,CAF 55 | Camerún,CMR,CAF 56 | Cabo Verde,CPV,CAF 57 | República Centroafricana,CTA,CAF 58 | Chad,CHA,CAF 59 | Comoras,COM,CAF 60 | Congo,CGO,CAF 61 | República Democrática del Congo,COD,CAF 62 | Costa de Marfil,CIV,CAF 63 | Yibuti,DJI,CAF 64 | Egipto,EGY,CAF 65 | Guinea Ecuatorial,EQG,CAF 66 | Eritrea,ERI,CAF 67 | Etiopía,ETH,CAF 68 | Gabón,GAB,CAF 69 | Gambia,GAM,CAF 70 | Ghana,GHA,CAF 71 | Guinea,GUI,CAF 72 | Guinea-Bisáu,GNB,CAF 73 | Kenia,KEN,CAF 74 | Lesoto,LES,CAF 75 | Liberia,LBR,CAF 76 | Libia,LBY,CAF 77 | Madagascar,MAD,CAF 78 | Malaui,MWI,CAF 79 | Malí,MLI,CAF 80 | Mauritania,MTN,CAF 81 | Mauricio,MRI,CAF 82 | Marruecos,MAR,CAF 83 | Mozambique,MOZ,CAF 84 | Namibia,NAM,CAF 85 | Níger,NIG,CAF 86 | Nigeria,NGA,CAF 87 | Reunión,REU,CAF 88 | Ruanda,RWA,CAF 89 | Santo Tomé y Príncipe,STP,CAF 90 | Senegal,SEN,CAF 91 | Seychelles,SEY,CAF 92 | Sierra Leona,SLE,CAF 93 | Somalia,SOM,CAF 94 | Sudáfrica,RSA,CAF 95 | Sudán del Sur,SSD,CAF 96 | Sudán,SDN,CAF 97 | Suazilandia,SWZ,CAF 98 | Tanzania,TAN,CAF 99 | Togo,TOG,CAF 100 | Túnez,TUN,CAF 101 | Uganda,UGA,CAF 102 | Zambia,ZAM,CAF 103 | Zimbabue,ZIM,CAF 104 | Anguila,AIA,CONCACAF 105 | Antigua y Barbuda,ATG,CONCACAF 106 | Aruba,ARU,CONCACAF 107 | Bahamas,BAH,CONCACAF 108 | Barbados,BRB,CONCACAF 109 | Belice,BLZ,CONCACAF 110 | Bermudas,BER,CONCACAF 111 | Islas Vírgenes Británicas,VGB,CONCACAF 112 | Canadá,CAN,CONCACAF 113 | Islas Caimán,CAY,CONCACAF 114 | Costa Rica,CRC,CONCACAF 115 | Cuba,CUB,CONCACAF 116 | Curazao,CUW,CONCACAF 117 | Dominica,DMA,CONCACAF 118 | República Dominicana,DOM,CONCACAF 119 | El Salvador,SLV,CONCACAF 120 | Guayana Francesa,GYF,CONCACAF 121 | Granada,GRN,CONCACAF 122 | Guadalupe,GPE,CONCACAF 123 | Guatemala,GUA,CONCACAF 124 | Guyana,GUY,CONCACAF 125 | Haití,HAI,CONCACAF 126 | Honduras,HON,CONCACAF 127 | Jamaica,JAM,CONCACAF 128 | Martinica,MTQ,CONCACAF 129 | México,MEX,CONCACAF 130 | Montserrat,MSR,CONCACAF 131 | Antillas Neerlandesas,ANT,CONCACAF 132 | Nicaragua,NCA,CONCACAF 133 | Panamá,PAN,CONCACAF 134 | Puerto Rico,PUR,CONCACAF 135 | San Cristóbal y Nieves,SKN,CONCACAF 136 | Santa Lucía,LCA,CONCACAF 137 | San Vicente y las Granadinas,VIN,CONCACAF 138 | San Martín,SMT,CONCACAF 139 | Sint Maarten,SXM,CONCACAF 140 | Surinam,SUR,CONCACAF 141 | Trinidad y Tobago,TRI,CONCACAF 142 | Islas Turcas y Caicos,TCA,CONCACAF 143 | Islas Vírgenes Estadounidenses,VIR,CONCACAF 144 | Estados Unidos,USA,CONCACAF 145 | Argentina,ARG,CONMEBOL 146 | Bolivia,BOL,CONMEBOL 147 | Brasil,BRA,CONMEBOL 148 | Chile,CHI,CONMEBOL 149 | Colombia,COL,CONMEBOL 150 | Ecuador,ECU,CONMEBOL 151 | Paraguay,PAR,CONMEBOL 152 | Perú,PER,CONMEBOL 153 | Uruguay,URU,CONMEBOL 154 | Venezuela,VEN,CONMEBOL 155 | Samoa Americana,ASA,OFC 156 | Islas Cook,COK,OFC 157 | Micronesia,FSM,OFC 158 | Fiyi,FIJ,OFC 159 | Kiribati,KIR,OFC 160 | Nueva Caledonia,NCL,OFC 161 | Nueva Zelanda,NZL,OFC 162 | Niue,NIU,OFC 163 | Palaos,PLW,OFC 164 | Papúa Nueva Guinea,PNG,OFC 165 | Samoa,SAM,OFC 166 | Islas Salomón,SOL,OFC 167 | Tahití,TAH,OFC 168 | Tonga,TGA,OFC 169 | Tuvalu,TUV,OFC 170 | Vanuatu,VAN,OFC 171 | Albania,ALB,UEFA 172 | Andorra,AND,UEFA 173 | Armenia,ARM,UEFA 174 | Austria,AUT,UEFA 175 | Azerbaiyán,AZE,UEFA 176 | Bielorrusia,BLR,UEFA 177 | Bélgica,BEL,UEFA 178 | Bosnia y Herzegovina,BIH,UEFA 179 | Bulgaria,BUL,UEFA 180 | Croacia,CRO,UEFA 181 | Chipre,CYP,UEFA 182 | República Checa,CZE,UEFA 183 | Dinamarca,DEN,UEFA 184 | Inglaterra,ENG,UEFA 185 | Estonia,EST,UEFA 186 | Islas Feroe,FRO,UEFA 187 | Finlandia,FIN,UEFA 188 | Francia,FRA,UEFA 189 | Macedonia,MKD,UEFA 190 | Georgia,GEO,UEFA 191 | Alemania,GER,UEFA 192 | Gibraltar,GIB,UEFA 193 | Grecia,GRE,UEFA 194 | Hungría,HUN,UEFA 195 | Islandia,ISL,UEFA 196 | Israel,ISR,UEFA 197 | Italia,ITA,UEFA 198 | Kazajistán,KAZ,UEFA 199 | Kosovo,KVX,UEFA 200 | Letonia,LVA,UEFA 201 | Liechtenstein,LIE,UEFA 202 | Lituania,LTU,UEFA 203 | Luxemburgo,LUX,UEFA 204 | Malta,MLT,UEFA 205 | Moldavia,MDA,UEFA 206 | Montenegro,MNE,UEFA 207 | Países Bajos,NED,UEFA 208 | Irlanda del Norte,NIR,UEFA 209 | Noruega,NOR,UEFA 210 | Polonia,POL,UEFA 211 | Portugal,POR,UEFA 212 | Irlanda,IRL,UEFA 213 | Rumania,ROU,UEFA 214 | Rusia,RUS,UEFA 215 | San Marino,SMR,UEFA 216 | Escocia,SCO,UEFA 217 | Serbia,SRB,UEFA 218 | Eslovaquia,SVK,UEFA 219 | Eslovenia,SVN,UEFA 220 | España,ESP,UEFA 221 | Suecia,SWE,UEFA 222 | Suiza,SUI,UEFA 223 | Turquía,TUR,UEFA 224 | Ucrania,UKR,UEFA 225 | Gales,WAL,UEFA 226 | -------------------------------------------------------------------------------- /marcotti/models/common/suppliers.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, Unicode, ForeignKey, Sequence 2 | from sqlalchemy.orm import relationship, backref 3 | 4 | from marcotti.models.common import BaseSchema 5 | 6 | 7 | class Suppliers(BaseSchema): 8 | __tablename__ = "suppliers" 9 | 10 | id = Column(Integer, Sequence('supplier_id_seq', start=1000), primary_key=True) 11 | name = Column(Unicode(40), nullable=False) 12 | 13 | def __repr__(self): 14 | return u"".format(self.id, self.name).encode('utf-8') 15 | 16 | 17 | class CompetitionMap(BaseSchema): 18 | __tablename__ = "competition_mapper" 19 | 20 | id = Column(Integer, ForeignKey('competitions.id'), primary_key=True) 21 | remote_id = Column(Integer, nullable=False, primary_key=True) 22 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 23 | 24 | supplier = relationship('Suppliers', backref=backref('competitions')) 25 | 26 | def __repr__(self): 27 | return "".format( 28 | self.id, self.remote_id, self.supplier.name) 29 | 30 | 31 | class CountryMap(BaseSchema): 32 | __tablename__ = "country_mapper" 33 | 34 | id = Column(Integer, ForeignKey('countries.id'), primary_key=True) 35 | remote_id = Column(Integer, nullable=False, primary_key=True) 36 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 37 | 38 | supplier = relationship('Suppliers', backref=backref('countries')) 39 | 40 | def __repr__(self): 41 | return "".format( 42 | self.id, self.remote_id, self.supplier.name) 43 | 44 | 45 | class ManagerMap(BaseSchema): 46 | __tablename__ = "manager_mapper" 47 | 48 | id = Column(Integer, ForeignKey('managers.id'), primary_key=True) 49 | remote_id = Column(Integer, nullable=False, primary_key=True) 50 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 51 | 52 | supplier = relationship('Suppliers', backref=backref('managers')) 53 | 54 | def __repr__(self): 55 | return "".format( 56 | self.id, self.remote_id, self.supplier.name) 57 | 58 | 59 | class MatchMap(BaseSchema): 60 | __tablename__ = "match_mapper" 61 | 62 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 63 | remote_id = Column(Integer, nullable=False, primary_key=True) 64 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 65 | 66 | supplier = relationship('Suppliers', backref=backref('matches')) 67 | 68 | def __repr__(self): 69 | return "".format( 70 | self.id, self.remote_id, self.supplier.name) 71 | 72 | 73 | class PlayerMap(BaseSchema): 74 | __tablename__ = "player_mapper" 75 | 76 | id = Column(Integer, ForeignKey('players.id'), primary_key=True) 77 | remote_id = Column(Integer, nullable=False, primary_key=True) 78 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 79 | 80 | supplier = relationship('Suppliers', backref=backref('players')) 81 | 82 | def __repr__(self): 83 | return "".format( 84 | self.id, self.remote_id, self.supplier.name) 85 | 86 | 87 | class PositionMap(BaseSchema): 88 | __tablename__ = "position_mapper" 89 | 90 | id = Column(Integer, ForeignKey('positions.id'), primary_key=True) 91 | remote_id = Column(Integer, nullable=False, primary_key=True) 92 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 93 | 94 | supplier = relationship('Suppliers', backref=backref('positions')) 95 | 96 | def __repr__(self): 97 | return "".format( 98 | self.id, self.remote_id, self.supplier.name) 99 | 100 | 101 | class RefereeMap(BaseSchema): 102 | __tablename__ = "referee_mapper" 103 | 104 | id = Column(Integer, ForeignKey('referees.id'), primary_key=True) 105 | remote_id = Column(Integer, nullable=False, primary_key=True) 106 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 107 | 108 | supplier = relationship('Suppliers', backref=backref('referees')) 109 | 110 | def __repr__(self): 111 | return "".format( 112 | self.id, self.remote_id, self.supplier.name) 113 | 114 | 115 | class SeasonMap(BaseSchema): 116 | __tablename__ = "season_mapper" 117 | 118 | id = Column(Integer, ForeignKey('seasons.id'), primary_key=True) 119 | remote_id = Column(Integer, nullable=False, primary_key=True) 120 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 121 | 122 | supplier = relationship('Suppliers', backref=backref('seasons')) 123 | 124 | def __repr__(self): 125 | return "".format( 126 | self.id, self.remote_id, self.supplier.name) 127 | 128 | 129 | class VenueMap(BaseSchema): 130 | __tablename__ = "venue_mapper" 131 | 132 | id = Column(Integer, ForeignKey('venues.id'), primary_key=True) 133 | remote_id = Column(Integer, nullable=False, primary_key=True) 134 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 135 | 136 | supplier = relationship('Suppliers', backref=backref('venues')) 137 | 138 | def __repr__(self): 139 | return "".format( 140 | self.id, self.remote_id, self.supplier.name) 141 | -------------------------------------------------------------------------------- /tests/test_match.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import pytest 4 | from sqlalchemy.exc import IntegrityError 5 | 6 | import marcotti.models.common.personnel as mcp 7 | import marcotti.models.common.match as mcm 8 | 9 | 10 | def test_match_generic_insert(session, match_data): 11 | match = mcm.Matches(**match_data) 12 | session.add(match) 13 | 14 | match_from_db = session.query(mcm.Matches).one() 15 | 16 | assert match_from_db.first_half_length == 45 17 | assert match_from_db.second_half_length == 45 18 | assert match_from_db.first_extra_length == 0 19 | assert match_from_db.second_extra_length == 0 20 | assert match_from_db.attendance == 0 21 | assert match_from_db.phase == 'matches' 22 | 23 | 24 | def test_match_negative_time_error(session, match_data): 25 | for field in ['first_half_length', 'second_half_length', 'first_extra_length', 'second_extra_length']: 26 | new_match_data = dict(match_data, **{field: -1}) 27 | match = mcm.Matches(**new_match_data) 28 | with pytest.raises(IntegrityError): 29 | session.add(match) 30 | session.commit() 31 | session.rollback() 32 | 33 | 34 | def test_match_negative_attendance_error(session, match_data): 35 | new_match_data = dict(match_data, **{'attendance': -1}) 36 | match = mcm.Matches(**new_match_data) 37 | with pytest.raises(IntegrityError): 38 | session.add(match) 39 | session.commit() 40 | session.rollback() 41 | 42 | 43 | def test_match_conditions_insert(session, match_data, match_condition_data): 44 | match_condition_data['match'] = mcm.Matches(**match_data) 45 | match_conditions = mcm.MatchConditions(**match_condition_data) 46 | session.add(match_conditions) 47 | 48 | match_from_db = session.query(mcm.Matches).one() 49 | conditions_from_db = session.query(mcm.MatchConditions).one() 50 | 51 | assert repr(conditions_from_db) == "".format(match_from_db.id) 53 | 54 | 55 | def test_match_conditions_temp_error(session, match_data, match_condition_data): 56 | match_condition_data['match'] = mcm.Matches(**match_data) 57 | for out_of_range in [-20.0, 55.0]: 58 | match_condition_data['kickoff_temp'] = out_of_range 59 | match_conditions = mcm.MatchConditions(**match_condition_data) 60 | with pytest.raises(IntegrityError): 61 | session.add(match_conditions) 62 | session.commit() 63 | session.rollback() 64 | 65 | 66 | def test_match_conditions_humid_error(session, match_data, match_condition_data): 67 | match_condition_data['match'] = mcm.Matches(**match_data) 68 | for out_of_range in [-1.0, 102.0]: 69 | match_condition_data['kickoff_humidity'] = out_of_range 70 | match_conditions = mcm.MatchConditions(**match_condition_data) 71 | with pytest.raises(IntegrityError): 72 | session.add(match_conditions) 73 | session.commit() 74 | session.rollback() 75 | 76 | 77 | def test_match_lineup_generic_insert(session, match_data, person_data, position_data): 78 | lineup = mcm.MatchLineups( 79 | match=mcm.Matches(**match_data), 80 | player=mcp.Players(**person_data['player'][1]), 81 | position=position_data[1] 82 | ) 83 | session.add(lineup) 84 | 85 | lineup_from_db = session.query(mcm.MatchLineups).one() 86 | match_from_db = session.query(mcm.Matches).one() 87 | player_from_db = session.query(mcp.Players).one() 88 | 89 | assert lineup_from_db.is_starting is False 90 | assert lineup_from_db.is_captain is False 91 | assert lineup_from_db.match_id == match_from_db.id 92 | assert lineup_from_db.player_id == player_from_db.id 93 | 94 | 95 | def test_lineup_designate_captain(session, match_data, person_data, position_data): 96 | capn_indx = 1 97 | lineups = [ 98 | mcm.MatchLineups( 99 | match=mcm.Matches(**match_data), 100 | player=mcp.Players(**plyr), 101 | position=pos, 102 | is_starting=True, 103 | is_captain=(j == capn_indx)) 104 | for j, (plyr, pos) in enumerate(zip(person_data['player'], position_data)) 105 | ] 106 | session.add_all(lineups) 107 | 108 | capn_position = position_data[capn_indx] 109 | 110 | lineup_from_db = session.query(mcm.MatchLineups).join(mcp.Positions).filter( 111 | mcp.Positions.name == capn_position.name).all() 112 | assert len(lineup_from_db) == 1 113 | assert lineup_from_db[0].is_captain is True 114 | 115 | other_lineup_from_db = session.query(mcm.MatchLineups).join(mcp.Positions).filter( 116 | mcp.Positions.name != capn_position.name).all() 117 | for others in other_lineup_from_db: 118 | assert others.is_captain is False 119 | 120 | 121 | def test_lineup_designate_starter(session, match_data, person_data, position_data): 122 | starter_indx = 0 123 | lineups = [ 124 | mcm.MatchLineups( 125 | match=mcm.Matches(**match_data), 126 | player=mcp.Players(**plyr), 127 | position=pos, 128 | is_starting=(j == starter_indx)) 129 | for j, (plyr, pos) in enumerate(zip(person_data['player'], position_data)) 130 | ] 131 | session.add_all(lineups) 132 | 133 | starter_position = position_data[starter_indx] 134 | 135 | lineup_from_db = session.query(mcm.MatchLineups).join(mcp.Positions).filter( 136 | mcp.Positions.name == starter_position.name).all() 137 | assert len(lineup_from_db) == 1 138 | assert lineup_from_db[0].is_starting is True 139 | assert lineup_from_db[0].is_captain is False 140 | 141 | other_lineup_from_db = session.query(mcm.MatchLineups).join(mcp.Positions).filter( 142 | mcp.Positions.name != starter_position.name).all() 143 | for others in other_lineup_from_db: 144 | assert others.is_starting is False 145 | assert others.is_captain is False 146 | -------------------------------------------------------------------------------- /marcotti/models/national.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from sqlalchemy import Column, Integer, ForeignKey 4 | from sqlalchemy.orm import relationship, backref 5 | from sqlalchemy.ext.declarative import declared_attr, declarative_base 6 | 7 | from models.common import BaseSchema 8 | import models.common.overview as mco 9 | import models.common.personnel as mcp 10 | import models.common.match as mcm 11 | import models.common.events as mce 12 | 13 | 14 | NatlSchema = declarative_base(name="National Teams", metadata=BaseSchema.metadata, 15 | class_registry=deepcopy(BaseSchema._decl_class_registry)) 16 | 17 | 18 | class NationalMixin(object): 19 | 20 | @declared_attr 21 | def team_id(cls): 22 | return Column(Integer, ForeignKey('countries.id')) 23 | 24 | 25 | class NationalMatchMixin(object): 26 | 27 | @declared_attr 28 | def home_team_id(cls): 29 | return Column(Integer, ForeignKey('countries.id')) 30 | 31 | @declared_attr 32 | def away_team_id(cls): 33 | return Column(Integer, ForeignKey('countries.id')) 34 | 35 | 36 | class FriendlyMixin(object): 37 | 38 | @declared_attr 39 | def home_team(cls): 40 | return relationship('Countries', foreign_keys="{}.home_team_id".format(cls.__name__), 41 | backref=backref('home_friendly_matches')) 42 | 43 | @declared_attr 44 | def away_team(cls): 45 | return relationship('Countries', foreign_keys="{}.away_team_id".format(cls.__name__), 46 | backref=backref('away_friendly_matches')) 47 | 48 | 49 | class GroupMixin(object): 50 | 51 | @declared_attr 52 | def home_team(cls): 53 | return relationship('Countries', foreign_keys="{}.home_team_id".format(cls.__name__), 54 | backref=backref('home_group_matches')) 55 | 56 | @declared_attr 57 | def away_team(cls): 58 | return relationship('Countries', foreign_keys="{}.away_team_id".format(cls.__name__), 59 | backref=backref('away_group_matches')) 60 | 61 | 62 | class KnockoutMixin(object): 63 | 64 | @declared_attr 65 | def home_team(cls): 66 | return relationship('Countries', foreign_keys="{}.home_team_id".format(cls.__name__), 67 | backref=backref('home_knockout_matches')) 68 | 69 | @declared_attr 70 | def away_team(cls): 71 | return relationship('Countries', foreign_keys="{}.away_team_id".format(cls.__name__), 72 | backref=backref('away_knockout_matches')) 73 | 74 | 75 | class NationalFriendlyMatches(FriendlyMixin, NationalMatchMixin, NatlSchema, mcm.Matches): 76 | __tablename__ = "natl_friendly_matches" 77 | __mapper_args__ = {'polymorphic_identity': 'natl_friendly'} 78 | 79 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 80 | 81 | def __repr__(self): 82 | return u"".format( 83 | self.home_team.name, self.away_team.name, self.competition.name, self.date.isoformat() 84 | ).encode('utf-8') 85 | 86 | def __unicode__(self): 87 | return u"".format( 88 | self.home_team.name, self.away_team.name, self.competition.name, self.date.isoformat() 89 | ) 90 | 91 | 92 | class NationalGroupMatches(GroupMixin, NationalMatchMixin, NatlSchema, mcm.GroupMatches, mcm.Matches): 93 | __tablename__ = "natl_group_matches" 94 | __mapper_args__ = {'polymorphic_identity': 'natl_group'} 95 | 96 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 97 | 98 | def __repr__(self): 99 | return u"".format( 100 | self.home_team.name, self.away_team.name, self.competition.name, self.group_round.value, 101 | self.group, self.matchday, self.date.isoformat() 102 | ).encode('utf-8') 103 | 104 | def __unicode__(self): 105 | return u"".format( 106 | self.home_team.name, self.away_team.name, self.competition.name, self.group_round.value, 107 | self.group, self.matchday, self.date.isoformat() 108 | ) 109 | 110 | 111 | class NationalKnockoutMatches(KnockoutMixin, NationalMatchMixin, NatlSchema, mcm.KnockoutMatches, mcm.Matches): 112 | __tablename__ = "natl_knockout_matches" 113 | __mapper_args__ = {'polymorphic_identity': 'natl_knockout'} 114 | 115 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 116 | 117 | def __repr__(self): 118 | return u"".format( 119 | self.home_team.name, self.away_team.name, self.competition.name, 120 | self.ko_round.value, self.matchday, self.date.isoformat() 121 | ).encode('utf-8') 122 | 123 | def __unicode__(self): 124 | return u"".format( 125 | self.home_team.name, self.away_team.name, self.competition.name, 126 | self.ko_round.value, self.matchday, self.date.isoformat() 127 | ) 128 | 129 | 130 | class NationalMatchLineups(NationalMixin, NatlSchema, mcm.MatchLineups): 131 | __tablename__ = "natl_match_lineups" 132 | __mapper_args__ = {'polymorphic_identity': 'national'} 133 | 134 | id = Column(Integer, ForeignKey('lineups.id'), primary_key=True) 135 | 136 | team = relationship('Countries', foreign_keys="NationalMatchLineups.team_id", backref=backref("lineups")) 137 | 138 | def __repr__(self): 139 | return u"".format( 140 | self.match_id, self.full_name, self.team.name, self.position.name, self.is_starting, self.is_captain 141 | ).encode('utf-8') 142 | 143 | def __unicode__(self): 144 | return u"".format( 145 | self.match_id, self.full_name, self.team.name, self.position.name, self.is_starting, self.is_captain 146 | ) 147 | 148 | 149 | class NationalGoals(NationalMixin, NatlSchema, mce.Goals): 150 | __tablename__ = 'natl_goals' 151 | __mapper_args__ = {'polymorphic_identity': 'national'} 152 | 153 | id = Column(Integer, ForeignKey('goals.id'), primary_key=True) 154 | 155 | team = relationship('Countries', foreign_keys="NationalGoals.team_id", backref=backref("goals")) 156 | -------------------------------------------------------------------------------- /tests/test_national.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import pytest 3 | 4 | import marcotti.models.national as mn 5 | import marcotti.models.common.overview as mco 6 | import marcotti.models.common.personnel as mcp 7 | import marcotti.models.common.enums as enums 8 | 9 | 10 | natl_only = pytest.mark.skipif( 11 | pytest.config.getoption("--schema") != "natl", 12 | reason="Test only valid for national team databases" 13 | ) 14 | 15 | 16 | @natl_only 17 | def test_natl_friendly_match_insert(session, national_data): 18 | friendly_match = mn.NationalFriendlyMatches(**national_data) 19 | session.add(friendly_match) 20 | 21 | match_from_db = session.query(mn.NationalFriendlyMatches).one() 22 | 23 | assert unicode(match_from_db) == u"" 25 | assert match_from_db.season.name == "1997-1998" 26 | assert match_from_db.competition.confederation.value == u"FIFA" 27 | assert match_from_db.venue.name == u"Emirates Stadium" 28 | assert match_from_db.home_manager.full_name == u"Arsène Wenger" 29 | assert match_from_db.away_manager.full_name == u"Gary Simpson" 30 | assert match_from_db.referee.full_name == u"Pierluigi Collina" 31 | 32 | 33 | @natl_only 34 | def test_natl_group_match_insert(session, national_data): 35 | group_match = mn.NationalGroupMatches( 36 | group_round=enums.GroupRoundType.first_round, 37 | group='C', 38 | matchday=2, 39 | **national_data 40 | ) 41 | session.add(group_match) 42 | 43 | match_from_db = session.query(mn.NationalGroupMatches).one() 44 | 45 | assert unicode(match_from_db) == u"" 47 | assert match_from_db.season.name == "1997-1998" 48 | assert match_from_db.competition.confederation.value == u"FIFA" 49 | assert match_from_db.venue.name == u"Emirates Stadium" 50 | assert match_from_db.home_manager.full_name == u"Arsène Wenger" 51 | assert match_from_db.away_manager.full_name == u"Gary Simpson" 52 | assert match_from_db.referee.full_name == u"Pierluigi Collina" 53 | 54 | 55 | @natl_only 56 | def test_natl_knockout_match_insert(session, national_data): 57 | knockout_match = mn.NationalKnockoutMatches( 58 | ko_round=enums.KnockoutRoundType.semifinal, 59 | **national_data 60 | ) 61 | session.add(knockout_match) 62 | 63 | match_from_db = session.query(mn.NationalKnockoutMatches).filter_by(ko_round=enums.KnockoutRoundType.semifinal) 64 | 65 | assert match_from_db.count() == 1 66 | assert unicode(match_from_db[0]) == u"" 69 | assert match_from_db[0].season.name == "1997-1998" 70 | assert match_from_db[0].competition.confederation.value == u"FIFA" 71 | assert match_from_db[0].venue.name == u"Emirates Stadium" 72 | assert match_from_db[0].home_manager.full_name == u"Arsène Wenger" 73 | assert match_from_db[0].away_manager.full_name == u"Gary Simpson" 74 | assert match_from_db[0].referee.full_name == u"Pierluigi Collina" 75 | 76 | 77 | @natl_only 78 | def test_natl_match_lineup_insert(session, national_data, person_data, position_data): 79 | match = mn.NationalGroupMatches( 80 | group_round=enums.GroupRoundType.first_round, 81 | group='C', 82 | matchday=2, 83 | **national_data 84 | ) 85 | session.add(match) 86 | session.commit() 87 | 88 | nation_from_db = session.query(mco.Countries).filter(mco.Countries.name == u"Mexico").one() 89 | 90 | player_data = person_data['player'][0] 91 | del player_data['country'] 92 | player_data['country_id'] = nation_from_db.id 93 | player = mcp.Players(position=position_data[0], **player_data) 94 | session.add(player) 95 | session.commit() 96 | 97 | lineup = mn.NationalMatchLineups( 98 | match_id=match.id, 99 | team_id=nation_from_db.id, 100 | player_id=player.id, 101 | position_id=player.position_id 102 | ) 103 | session.add(lineup) 104 | 105 | lineup_from_db = session.query(mn.NationalMatchLineups).join(mn.NationalGroupMatches).\ 106 | filter(mn.NationalGroupMatches.id == match.id) 107 | 108 | assert lineup_from_db.count() == 1 109 | assert unicode(lineup_from_db[0]) == u"".format(match.id) 111 | 112 | 113 | @natl_only 114 | def test_natl_goal_insert(session, national_data, person_data, position_data): 115 | match = mn.NationalKnockoutMatches( 116 | ko_round=enums.KnockoutRoundType.round_16, 117 | **national_data 118 | ) 119 | session.add(match) 120 | session.commit() 121 | 122 | nation_from_db = session.query(mco.Countries).filter(mco.Countries.name == u"Mexico").one() 123 | 124 | player_data = person_data['player'][0] 125 | del player_data['country'] 126 | player_data['country_id'] = nation_from_db.id 127 | player = mcp.Players(position=position_data[0], **player_data) 128 | session.add(player) 129 | session.commit() 130 | 131 | lineup = mn.NationalMatchLineups( 132 | match_id=match.id, 133 | team_id=nation_from_db.id, 134 | player_id=player.id, 135 | position_id=player.position_id 136 | ) 137 | session.add(lineup) 138 | session.commit() 139 | 140 | goal = mn.NationalGoals( 141 | lineup_id=lineup.id, 142 | team_id=nation_from_db.id, 143 | bodypart=enums.BodypartType.head, 144 | event=enums.ShotEventType.cross_ck, 145 | time=70 146 | ) 147 | session.add(goal) 148 | 149 | goals_from_db = session.query(mn.NationalGoals).join(mn.NationalMatchLineups)\ 150 | .join(mn.NationalKnockoutMatches).filter(mn.NationalKnockoutMatches.id == match.id) 151 | 152 | assert goals_from_db.count() == 1 153 | assert goals_from_db[0].team.name == u"Mexico" 154 | assert goals_from_db[0].lineup.full_name == u"Miguel Ángel Ponce" 155 | assert goals_from_db[0].bodypart.value == "Head" 156 | assert goals_from_db[0].event.value == "Cross from corner kick" 157 | assert goals_from_db[0].time == 70 158 | 159 | 160 | @natl_only 161 | def test_natl_penalty_shootout_opener_insert(session, national_data): 162 | match = mn.NationalKnockoutMatches( 163 | ko_round=enums.KnockoutRoundType.final, 164 | **national_data 165 | ) 166 | session.add(match) 167 | session.commit() 168 | 169 | result = session.query(mn.NationalKnockoutMatches.home_team_id, mn.NationalKnockoutMatches.away_team_id)\ 170 | .filter_by(id=match.id) 171 | 172 | home, away = result[0] 173 | 174 | shootout = mn.NationalPenaltyShootoutOpeners(match_id=match.id, team_id=away) 175 | session.add(shootout) 176 | 177 | shootout_from_db = session.query(mn.NationalPenaltyShootoutOpeners)\ 178 | .filter(mn.NationalPenaltyShootoutOpeners.match_id == match.id).one() 179 | 180 | assert unicode(shootout_from_db) == u"".format(match.id) 181 | -------------------------------------------------------------------------------- /marcotti/tools/dbsetup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pip 3 | import pkg_resources 4 | 5 | import jinja2 6 | from clint.textui import colored, prompt, puts, validators 7 | 8 | 9 | db_ports = { 10 | 'postgresql': '5432', 11 | 'mysql': '3306', 12 | 'mssql': '1433', 13 | 'oracle': '1521', 14 | 'firebird': '3050' 15 | } 16 | 17 | db_modules = { 18 | 'postgresql': 'psycopg2>=2.6.1', 19 | 'mssql': 'pyodbc>=3.0', 20 | 'mysql': 'mysql-python>=1.2.3', 21 | 'oracle': 'cx_oracle>=5.0', 22 | 'firebird': 'fdb>=1.6' 23 | } 24 | 25 | dialect_options = [{'selector': '1', 'prompt': 'PostgreSQL', 'return': 'postgresql'}, 26 | {'selector': '2', 'prompt': 'MySQL', 'return': 'mysql'}, 27 | {'selector': '3', 'prompt': 'SQL Server', 'return': 'mssql'}, 28 | {'selector': '4', 'prompt': 'Oracle', 'return': 'oracle'}, 29 | {'selector': '5', 'prompt': 'Firebird', 'return': 'firebird'}, 30 | {'selector': '6', 'prompt': 'SQLite', 'return': 'sqlite'}] 31 | 32 | 33 | binary_options = [{'selector': '1', 'prompt': 'Yes', 'return': True}, 34 | {'selector': '2', 'prompt': 'No', 'return': False}] 35 | 36 | 37 | def path_query(query_string): 38 | path_txt = prompt.query(query_string, validators=[]) 39 | return None if path_txt == '' else os.path.split(path_txt) 40 | 41 | 42 | def setup_user_input(): 43 | """ 44 | Setup configuration and database loading script by querying information from user. 45 | """ 46 | print("#### Please answer the following questions to setup the folder ####") 47 | log_folder = prompt.query('Logging folder (must exist):', default='.', validators=[validators.PathValidator()]) 48 | loader_file = prompt.query('Loader file name:', default='loader') 49 | config_file = prompt.query('Config file name:', default='local') 50 | config_class = prompt.query('Config class name:', default='LocalConfig') 51 | print("#### Database configuration setup ####") 52 | dialect = prompt.options('Marcotti-Events Database backend:', dialect_options) 53 | if dialect == 'sqlite': 54 | dbname = prompt.query('Database filename (must exist):', validators=[validators.FileValidator()]) 55 | dbuser = '' 56 | hostname = '' 57 | dbport = 0 58 | else: 59 | dbname = prompt.query('Database name:') 60 | dbuser = prompt.query('Database user:', default='') 61 | puts(colored.red('Database password is not defined -- You must define it in the config file!')) 62 | hostname = prompt.query('Database hostname:', default='localhost') 63 | dbport = prompt.query('Database path:', default=db_ports.get(dialect)) 64 | print("#### Database season setup ####") 65 | start_yr = prompt.query('Start season year', default='1990', validators=[validators.IntegerValidator()]) 66 | end_yr = prompt.query('End season year', default='2020', validators=[validators.IntegerValidator()]) 67 | print("#### Data file setup ####") 68 | supplier = prompt.query('Name of data supplier:') 69 | is_club_db = prompt.options('Is this a club database?', binary_options) 70 | spanish = prompt.options('Are country names in Spanish?', binary_options) 71 | csv_data_dir = prompt.query('Directory containing CSV data files:', default='.', 72 | validators=[validators.PathValidator()]) 73 | supplier_data_path = path_query('Relative path of Suppliers CSV data files:') 74 | club_data_path = path_query('Relative path of Clubs CSV data files:') 75 | comp_data_path = path_query('Relative path of Competitions CSV data files:') 76 | season_data_path = path_query('Relative path of Seasons CSV data files:') 77 | venue_data_path = path_query('Relative path of Venues CSV data files:') 78 | position_data_path = path_query('Relative path of Player Positions CSV data files:') 79 | player_data_path = path_query('Relative path of Players CSV data files:') 80 | manager_data_path = path_query('Relative path of Managers CSV data files:') 81 | referee_data_path = path_query('Relative path of Referees CSV data files:') 82 | league_match_data_path = path_query('Relative path of League Matches CSV data files:') 83 | group_match_data_path = path_query('Relative path of Group Matches CSV data files:') 84 | knockout_match_data_path = path_query('Relative path of Knockout Matches CSV data files:') 85 | goal_data_path = path_query('Relative path of Goals CSV data files:') 86 | penalty_data_path = path_query('Relative path of Penalties CSV data files:') 87 | bookable_data_path = path_query('Relative path of Bookables CSV data files:') 88 | substitution_data_path = path_query('Relative path of Substitutions CSV data files:') 89 | shootout_data_path = path_query('Relative path of Penalty Shootouts CSV data files:') 90 | player_stats_data_path = path_query('Relative path of Player Statistics CSV data files:') 91 | 92 | print("#### End setup questions ####") 93 | 94 | setup_dict = { 95 | 'loader_file': loader_file.lower(), 96 | 'config_file': config_file.lower(), 97 | 'config_class': config_class, 98 | 'supplier': supplier, 99 | 'dialect': dialect, 100 | 'dbname': dbname, 101 | 'dbuser': dbuser, 102 | 'dbhost': hostname, 103 | 'dbport': dbport, 104 | 'start_yr': start_yr, 105 | 'end_yr': end_yr, 106 | 'logging_dir': log_folder, 107 | 'log_file_path': os.path.join(log_folder, 'marcotti.log'), 108 | 'club_db': is_club_db, 109 | 'country_prefix': 'es' * (spanish is True), 110 | 'csv_data_dir': csv_data_dir, 111 | 'csv_data': { 112 | 'suppliers': supplier_data_path, 113 | 'competitions': comp_data_path, 114 | 'seasons': season_data_path, 115 | 'clubs': club_data_path, 116 | 'venues': venue_data_path, 117 | 'positions': position_data_path, 118 | 'players': player_data_path, 119 | 'managers': manager_data_path, 120 | 'referees': referee_data_path, 121 | 'league_matches': league_match_data_path, 122 | 'group_matches': group_match_data_path, 123 | 'knockout_matches': knockout_match_data_path, 124 | 'goals': goal_data_path, 125 | 'penalties': penalty_data_path, 126 | 'bookables': bookable_data_path, 127 | 'substitutions': substitution_data_path, 128 | 'shootouts': shootout_data_path, 129 | 'statistics': player_stats_data_path 130 | } 131 | } 132 | return setup_dict 133 | 134 | 135 | def main(): 136 | """ 137 | Main function exposed as script command. 138 | """ 139 | DATA_PATH = pkg_resources.resource_filename('marcotti', 'data/') 140 | setup_dict = setup_user_input() 141 | print("#### Installing database driver ####") 142 | if setup_dict['dialect'] == 'sqlite': 143 | print('SQLite database is used -- no external driver needed') 144 | else: 145 | pip.main(['install', db_modules.get(setup_dict['dialect'])]) 146 | print("#### Creating settings and data loader modules ####") 147 | env = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=DATA_PATH), 148 | trim_blocks=True, lstrip_blocks=True) 149 | template_files = ['local.skel', 'logging.skel', 'loader.skel'] 150 | output_files = ['{config_file}.py'.format(**setup_dict), 151 | 'logging.json', 152 | '{loader_file}.py'.format(**setup_dict)] 153 | for template_file, output_file in zip(template_files, output_files): 154 | template = env.get_template(os.path.join('templates', template_file)) 155 | with open(output_file, 'w') as g: 156 | result = template.render(setup_dict) 157 | g.write(result) 158 | print("Configured {}".format(output_file)) 159 | print("#### Setup complete ####") 160 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import pytest 4 | from sqlalchemy.orm import aliased 5 | from sqlalchemy.exc import IntegrityError 6 | 7 | import marcotti.models.common.enums as enums 8 | import marcotti.models.common.personnel as mcp 9 | import marcotti.models.common.match as mcm 10 | import marcotti.models.common.events as mce 11 | 12 | 13 | @pytest.fixture 14 | def match_lineup(session, match_data, person_data, position_data): 15 | match = mcm.Matches(**match_data) 16 | session.add(match) 17 | 18 | match_from_db = session.query(mcm.Matches).one() 19 | 20 | lineups = [ 21 | mcm.MatchLineups( 22 | match_id=match_from_db.id, 23 | player=mcp.Players(**plyr), 24 | position=pos, 25 | is_starting=True, 26 | is_captain=False) 27 | for j, (plyr, pos) in enumerate(zip(person_data['player'], position_data)) 28 | ] 29 | session.add_all(lineups) 30 | 31 | scorer_indx = 1 32 | 33 | lineup_from_db = session.query(mcm.MatchLineups).join(mcp.Players).\ 34 | filter(mcp.Players.last_name == person_data['player'][scorer_indx]['last_name']).one() 35 | 36 | return match_from_db, lineup_from_db 37 | 38 | 39 | def test_goals_generic_insert(session, match_lineup): 40 | match_from_db, lineup_from_db = match_lineup 41 | 42 | goal = mce.Goals( 43 | lineup_id=lineup_from_db.id, 44 | bodypart=enums.BodypartType.left_foot, 45 | event=enums.ShotEventType.shot_18_box, 46 | time=65 47 | ) 48 | session.add(goal) 49 | 50 | goals_from_db = session.query(mce.Goals).join(mcm.MatchLineups).join(mcm.Matches).\ 51 | filter(mcm.Matches.id == match_from_db.id) 52 | 53 | assert goals_from_db.count() == 1 54 | assert goals_from_db[0].time == 65 55 | assert goals_from_db[0].stoppage == 0 56 | assert goals_from_db[0].bodypart.value == "Left foot" 57 | assert goals_from_db[0].event.value == "Shot inside penalty area" 58 | assert len(lineup_from_db.goals) == 1 59 | 60 | 61 | def test_event_missing_time_error(session, match_lineup): 62 | match_from_db, lineup_from_db = match_lineup 63 | 64 | with pytest.raises(IntegrityError): 65 | goal = mce.Goals( 66 | lineup_id=lineup_from_db.id, 67 | bodypart=enums.BodypartType.left_foot, 68 | event=enums.ShotEventType.shot_18_box 69 | ) 70 | session.add(goal) 71 | session.commit() 72 | 73 | 74 | def test_event_time_out_of_range_error(session, match_lineup): 75 | match_from_db, lineup_from_db = match_lineup 76 | 77 | for out_of_range in [-1, 0, 121]: 78 | with pytest.raises(IntegrityError): 79 | goal = mce.Goals( 80 | lineup_id=lineup_from_db.id, 81 | bodypart=enums.BodypartType.left_foot, 82 | event=enums.ShotEventType.shot_18_box, 83 | time=out_of_range 84 | ) 85 | session.add(goal) 86 | session.commit() 87 | session.rollback() 88 | 89 | 90 | def test_event_stoppage_time_out_of_range_error(session, match_lineup): 91 | match_from_db, lineup_from_db = match_lineup 92 | 93 | for out_of_range in [-1, 20]: 94 | with pytest.raises(IntegrityError): 95 | goal = mce.Goals( 96 | lineup_id=lineup_from_db.id, 97 | bodypart=enums.BodypartType.left_foot, 98 | event=enums.ShotEventType.shot_18_box, 99 | time=90, 100 | stoppage=out_of_range 101 | ) 102 | session.add(goal) 103 | session.commit() 104 | session.rollback() 105 | 106 | 107 | def test_penalties_insert(session, match_lineup): 108 | match_from_db, lineup_from_db = match_lineup 109 | 110 | penalty = mce.Penalties( 111 | lineup_id = lineup_from_db.id, 112 | foul=enums.FoulEventType.handball, 113 | outcome=enums.ShotOutcomeType.goal, 114 | time=77 115 | ) 116 | session.add(penalty) 117 | 118 | penalties_from_db = session.query(mce.Penalties).filter(mce.Penalties.outcome == enums.ShotOutcomeType.goal) 119 | 120 | assert penalties_from_db.count() == 1 121 | assert penalties_from_db[0].foul.value == "Handball" 122 | assert penalties_from_db[0].lineup == lineup_from_db 123 | assert len(lineup_from_db.penalties) == 1 124 | 125 | 126 | def test_bookable_offense_insert(session, match_lineup): 127 | match_from_db, lineup_from_db = match_lineup 128 | 129 | offense = mce.Bookables( 130 | lineup_id = lineup_from_db.id, 131 | foul=enums.FoulEventType.repeated_fouling, 132 | card=enums.CardType.yellow, 133 | time=53 134 | ) 135 | session.add(offense) 136 | 137 | offenses_from_db = session.query(mce.Bookables).join(mcm.MatchLineups).\ 138 | filter(mcm.MatchLineups.id == lineup_from_db.id) 139 | 140 | assert offenses_from_db.count() == 1 141 | assert offenses_from_db[0].card.value == "Yellow" 142 | assert offenses_from_db[0].foul.value == "Persistent infringement" 143 | assert offenses_from_db[0].time == 53 144 | assert len(lineup_from_db.bookables) == 1 145 | 146 | 147 | def test_substitutions_insert(session, match_lineup, person_data): 148 | match_from_db, lineup_from_db = match_lineup 149 | 150 | bench_lineup = mcm.MatchLineups( 151 | match_id=match_from_db.id, 152 | player=mcp.Players(**person_data['generic']), 153 | position=mcp.Positions(name=u"Center back", type=enums.PositionType.defender), 154 | is_starting=False, 155 | is_captain=False 156 | ) 157 | session.add(bench_lineup) 158 | session.commit() 159 | 160 | substitution = mce.Substitutions( 161 | lineup_in_id=bench_lineup.id, 162 | lineup_out_id=lineup_from_db.id, 163 | time=67 164 | ) 165 | session.add(substitution) 166 | 167 | lineup_alias = aliased(mcm.MatchLineups) 168 | substitution_from_db = session.query(mce.Substitutions)\ 169 | .join(mcm.MatchLineups, mcm.MatchLineups.id == mce.Substitutions.lineup_in_id)\ 170 | .join(lineup_alias, lineup_alias.id == mce.Substitutions.lineup_out_id)\ 171 | .join(mcm.Matches).filter(mcm.Matches.id == match_from_db.id) 172 | 173 | assert substitution_from_db.count() == 1 174 | assert substitution_from_db[0].lineup_out.full_name == u"Cristiano Ronaldo" 175 | assert substitution_from_db[0].lineup_in.full_name == u"John Doe" 176 | assert substitution_from_db[0].time == 67 177 | 178 | 179 | def test_retirement_insert(session, match_lineup): 180 | match_from_db, lineup_from_db = match_lineup 181 | 182 | withdrawal = mce.Substitutions(lineup_out_id=lineup_from_db.id, time=85) 183 | session.add(withdrawal) 184 | 185 | lineup_alias = aliased(mcm.MatchLineups) 186 | withdrawal_from_db = session.query(mce.Substitutions)\ 187 | .outerjoin(mcm.MatchLineups, mcm.MatchLineups.id == mce.Substitutions.lineup_in_id)\ 188 | .join(lineup_alias, lineup_alias.id == mce.Substitutions.lineup_out_id)\ 189 | .join(mcm.Matches).filter(mcm.Matches.id == match_from_db.id) 190 | 191 | assert withdrawal_from_db.count() == 1 192 | assert withdrawal_from_db[0].lineup_out.full_name == u"Cristiano Ronaldo" 193 | assert withdrawal_from_db[0].lineup_in is None 194 | assert withdrawal_from_db[0].time == 85 195 | 196 | 197 | def test_penalty_shootouts_insert(session, match_lineup): 198 | match_from_db, lineup_from_db = match_lineup 199 | 200 | shootout = mce.PenaltyShootouts( 201 | lineup_id=lineup_from_db.id, 202 | round=4, 203 | outcome=enums.ShotOutcomeType.over 204 | ) 205 | session.add(shootout) 206 | 207 | shootout_from_db = session.query(mce.PenaltyShootouts).join(mcm.MatchLineups)\ 208 | .join(mcm.Matches).filter(mcm.Matches.id == match_from_db.id) 209 | 210 | assert shootout_from_db.count() == 1 211 | assert shootout_from_db[0].round == 4 212 | assert shootout_from_db[0].outcome.value == "Over crossbar" 213 | assert shootout_from_db[0].lineup.full_name == u"Cristiano Ronaldo" 214 | -------------------------------------------------------------------------------- /marcotti/models/common/enums.py: -------------------------------------------------------------------------------- 1 | from models.common import DeclEnum 2 | 3 | 4 | class GroupRoundType(DeclEnum): 5 | """ 6 | Enumerated names of rounds in group stages of football competitions. 7 | """ 8 | group_stage = "Group Stage", "Group Stage" 9 | first_round = "First Round", "First Round" 10 | second_round = "Second Round", "Second Round" 11 | third_round = "Third Round", "Third Round" 12 | fourth_round = "Fourth Round", "Fourth Round" 13 | final_round = "Final Round", "Final Round" 14 | playoff = "Playoff Group", "Playoff Group" 15 | championship = "Championship Group", "Championship Group" 16 | promotion = "Promotion Group", "Promotion Group" 17 | relegation = "Relegation Group", "Relegation Group" 18 | 19 | 20 | class KnockoutRoundType(DeclEnum): 21 | """ 22 | Enumerated names of rounds in knockout stages of football competitions. 23 | """ 24 | extra_prelim = "Extra Preliminary Round", "Extra Preliminary Round" 25 | prelim = "Preliminary Round", "Preliminary Round" 26 | first_qualifying = "First Qualifying Round", "First Qualifying Round" 27 | second_qualifying = "Second Qualifying Round", "Second Qualifying Round" 28 | third_qualifying = "Third Qualifying Round", "Third Qualifying Round" 29 | fourth_qualifying = "Fourth Qualifying Round", "Fourth Qualifying Round" 30 | playoff = "Playoff Round", "Playoff Round" 31 | first_round = "First Round", "First Round" 32 | second_round = "Second Round", "Second Round" 33 | third_round = "Third Round", "Third Round" 34 | fourth_round = "Fourth Round", "Fourth Round" 35 | fifth_round = "Fifth Round", "Fifth Round" 36 | sixth_round = "Sixth Round", "Sixth Round" 37 | seventh_round = "Seventh Round", "Seventh Round" 38 | eighth_round = "Eighth Round", "Eighth Round" 39 | round_64 = "Round of 64 (1/32)", "Round of 64 (1/32)" 40 | round_32 = "Round of 32 (1/16)", "Round of 32 (1/16)" 41 | round_16 = "Round of 16 (1/8)", "Round of 16 (1/8)" 42 | quarterfinal = "Quarterfinal (1/4)", "Quarterfinal (1/4)" 43 | semifinal = "Semi-Final (1/2)", "Semi-Final (1/2)" 44 | final = "Final", "Final" 45 | qualifying_final = "Qualifying Final", "Qualifying Final" 46 | prelim_final = "Preliminary Final", "Preliminary Final" 47 | grand_final = "Grand Final", "Grand Final" 48 | 49 | 50 | class ConfederationType(DeclEnum): 51 | """ 52 | Enumerated names of the international football confederations. 53 | """ 54 | africa = "CAF", "Confederation of African Football" 55 | asia = "AFC", "Asian Football Confederation" 56 | europe = "UEFA", "Union of European Football Associations" 57 | north_america = "CONCACAF", "Confederation of North, Central American, and Caribbean Association Football" 58 | oceania = "OFC", "Oceania Football Confederation" 59 | south_america = "CONMEBOL", "South American Football Confederation" 60 | fifa = "FIFA", "International Federation of Association Football" 61 | 62 | 63 | class PositionType(DeclEnum): 64 | """ 65 | Enumerated categories of football player positions. 66 | """ 67 | goalkeeper = "Goalkeeper", "Goalkeepers" 68 | defender = "Defender", "Defending positions" 69 | midfielder = "Midfielder", "Midfield positions" 70 | forward = "Forward", "Forward positions" 71 | unknown = "Unknown", "Unknown player position" 72 | 73 | 74 | class NameOrderType(DeclEnum): 75 | """ 76 | Enumerated types of naming order conventions. 77 | """ 78 | western = "Western", "Western" 79 | middle = "Middle", "Middle" 80 | eastern = "Eastern", "Eastern" 81 | 82 | 83 | class CardType(DeclEnum): 84 | """ 85 | Enumerated types of disciplinary cards. 86 | """ 87 | yellow = "Yellow", "Yellow" 88 | yellow_red = "Yellow/Red", "Yellow/Red" 89 | red = "Red", "Red" 90 | 91 | 92 | class SurfaceType(DeclEnum): 93 | """ 94 | Enumerated types of playing surfaces. 95 | """ 96 | natural = "Natural", "Natural" 97 | artificial = "Artificial", "Artificial" 98 | hybrid = "Hybrid", "Hybrid" 99 | 100 | 101 | class ShotOutcomeType(DeclEnum): 102 | """ 103 | Enumerated types of shot outcomes. 104 | """ 105 | goal = "Goal", "Goal" 106 | miss = "Miss", "Miss" 107 | save = "Save", "Save" 108 | wide = "Wide of post", "Wide of post" 109 | over = "Over crossbar", "Over crossbar" 110 | post = "Hit post", "Hit post" 111 | bar = "Hit crossbar", "Hit crossbar" 112 | 113 | 114 | class BodypartType(DeclEnum): 115 | """ 116 | Enumerated types of body parts. 117 | """ 118 | left_foot = "Left foot", "Left foot" 119 | right_foot = "Right foot", "Right foot" 120 | foot = "Foot", "Foot" 121 | head = "Head", "Head" 122 | chest = "Chest", "Chest" 123 | other = "Other", "Other body part" 124 | unknown = "Unknown", "Unknown" 125 | 126 | 127 | class ShotEventType(DeclEnum): 128 | """ 129 | Enumerated types of shot events. 130 | """ 131 | unknown = "Unknown", "Unknown" 132 | cross_fk = "Cross from free kick", "Cross from free kick" 133 | cross_ck = "Cross from corner kick", "Cross from corner kick" 134 | cross_throw = "Cross from throw-in", "Cross from throw-in" 135 | cross_open_play = "Cross from open play", "Cross from open play" 136 | olympic = "Direct from corner kick", "Direct from corner kick" 137 | free_kick = "Direct from free kick", "Direct from free kick" 138 | flick_ck = "Flick on from corner kick", "Flick on from corner kick" 139 | flick_fk = "Flick on from direct free kick", "Flick on from direct free kick" 140 | flick_ifk = "Flick on from indirect free kick", "Flick on from indirect free kick" 141 | flick_throw = "Flick on from throw-in", "Flick on from throw-in" 142 | one_v_one = "Through pass creates 1-v-1", "Through pass creates 1-v-1" 143 | scramble = "Contested scramble", "Contested scramble" 144 | redirected = "Close-range re-direction", "Close-range re-direction" 145 | deflection = "Deflected shot", "Deflected shot" 146 | shot_6_box = "Shot inside goal area", "Shot inside goal area" 147 | shot_18_box = "Shot inside penalty area", "Shot inside penalty area" 148 | shot_outside = "Shot outside penalty area", "Shot outside penalty area" 149 | rebound = "Shot following rebound", "Shot following rebound" 150 | giveaway = "Shot following defensive giveaway", "Shot following defensive giveaway" 151 | round_keeper = "Maneuver around goalkeeper", "Maneuver around goalkeeper" 152 | 153 | 154 | class FoulEventType(DeclEnum): 155 | """ 156 | Enumerated types of foul events. 157 | """ 158 | unknown = "Unknown", "Unknown" 159 | handball = "Handball", "Handball" 160 | holding = "Holding", "Holding" 161 | off_ball = "Off-ball infraction", "Off-ball infraction" 162 | dangerous = "Dangerous play", "Dangerous play" 163 | reckless = "Reckless challenge", "Reckless challenge" 164 | over_celebration = "Excessive celebration", "Excessive celebration" 165 | simulation = "Simulation", "Simulation" 166 | dissent = "Dissent", "Dissent" 167 | repeated_fouling = "Persistent infringement", "Persistent infringement" 168 | delay_restart = "Delaying restart", "Delaying restart" 169 | encroachment = "Dead ball encroachment", "Dead ball encroachment" 170 | field_unauthorized = "Unauthorized field entry/exit", "Unauthorized field entry/exit" 171 | serious_foul_play = "Serious foul play", "Serious foul play" 172 | violent_conduct = "Violent conduct", "Violent conduct" 173 | verbal_abuse = "Offensive/abusive language or gestures", "Offensive/abusive language or gestures" 174 | spitting = "Spitting", "Spitting" 175 | professional = "Professional foul", "Professional foul" 176 | unsporting = "Unsporting behavior", "Unsporting behavior" 177 | handball_block_goal = "Handball denied obvious scoring opportunity", "Handball denied obvious scoring opportunity" 178 | 179 | 180 | class WeatherConditionType(DeclEnum): 181 | """ 182 | Enumerated types of NWS/NOAA weather conditions. 183 | """ 184 | clear = "Clear", "Clear" 185 | partly_cloudy = "Partly Cloudy", "Partly Cloudy" 186 | mostly_cloudy = "Mostly Cloudy", "Mostly Cloudy" 187 | few_clouds = "Few Clouds", "Few Clouds" 188 | dry_hot = "Hot and Dry", "Hot and Dry" 189 | humid_hot = "Hot and Humid", "Hot and Humid" 190 | overcast = "Overcast", "Overcast" 191 | fog = "Fog/Mist", "Fog/Mist" 192 | light_rain = "Light Rain", "Light Rain" 193 | rain = "Rain", "Rain" 194 | heavy_rain = "Heavy Rain", "Heavy Rain" 195 | windy_clear = "Clear and Windy", "Clear and Windy" 196 | windy_mostly_cloudy = "Mostly Cloudy and Windy", "Mostly Cloudy and Windy" 197 | windy_partly_cloudy = "Partly Cloudy and Windy", "Partly Cloudy and Windy" 198 | windy_overcast = "Overcast and Windy", "Overcast and Windy" 199 | flurries = "Snow Flurries", "Snow Flurries" 200 | light_snow = "Light Snow", "Light Snow" 201 | heavy_snow = "Heavy Snow", "Heavy Snow" 202 | -------------------------------------------------------------------------------- /marcotti/models/club.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from sqlalchemy import Column, Integer, Sequence, ForeignKey, Unicode 4 | from sqlalchemy.orm import relationship, backref 5 | from sqlalchemy.ext.declarative import declared_attr, declarative_base 6 | 7 | from models.common import BaseSchema 8 | import models.common.suppliers as mcs 9 | import models.common.overview as mco 10 | import models.common.personnel as mcp 11 | import models.common.match as mcm 12 | import models.common.events as mce 13 | 14 | 15 | ClubSchema = declarative_base(name="Clubs", metadata=BaseSchema.metadata, 16 | class_registry=deepcopy(BaseSchema._decl_class_registry)) 17 | 18 | 19 | class Clubs(ClubSchema): 20 | __tablename__ = 'clubs' 21 | 22 | id = Column(Integer, Sequence('club_id_seq', start=10000), primary_key=True) 23 | 24 | name = Column(Unicode(60)) 25 | 26 | country_id = Column(Integer, ForeignKey('countries.id')) 27 | country = relationship('Countries', backref=backref('clubs')) 28 | 29 | def __repr__(self): 30 | return "".format(self.name, self.country.name) 31 | 32 | def __unicode__(self): 33 | return u"".format(self.name, self.country.name) 34 | 35 | 36 | class ClubMixin(object): 37 | 38 | @declared_attr 39 | def team_id(cls): 40 | return Column(Integer, ForeignKey('clubs.id')) 41 | 42 | 43 | class ClubMatchMixin(object): 44 | 45 | @declared_attr 46 | def home_team_id(cls): 47 | return Column(Integer, ForeignKey('clubs.id')) 48 | 49 | @declared_attr 50 | def away_team_id(cls): 51 | return Column(Integer, ForeignKey('clubs.id')) 52 | 53 | 54 | class FriendlyMixin(object): 55 | 56 | @declared_attr 57 | def home_team(cls): 58 | return relationship('Clubs', foreign_keys="{}.home_team_id".format(cls.__name__), 59 | backref=backref('home_friendly_matches')) 60 | 61 | @declared_attr 62 | def away_team(cls): 63 | return relationship('Clubs', foreign_keys="{}.away_team_id".format(cls.__name__), 64 | backref=backref('away_friendly_matches')) 65 | 66 | 67 | class LeagueMixin(object): 68 | 69 | @declared_attr 70 | def home_team(cls): 71 | return relationship('Clubs', foreign_keys="{}.home_team_id".format(cls.__name__), 72 | backref=backref('home_league_matches')) 73 | 74 | @declared_attr 75 | def away_team(cls): 76 | return relationship('Clubs', foreign_keys="{}.away_team_id".format(cls.__name__), 77 | backref=backref('away_league_matches')) 78 | 79 | 80 | class GroupMixin(object): 81 | 82 | @declared_attr 83 | def home_team(cls): 84 | return relationship('Clubs', foreign_keys="{}.home_team_id".format(cls.__name__), 85 | backref=backref('home_group_matches')) 86 | 87 | @declared_attr 88 | def away_team(cls): 89 | return relationship('Clubs', foreign_keys="{}.away_team_id".format(cls.__name__), 90 | backref=backref('away_group_matches')) 91 | 92 | 93 | class KnockoutMixin(object): 94 | 95 | @declared_attr 96 | def home_team(cls): 97 | return relationship('Clubs', foreign_keys="{}.home_team_id".format(cls.__name__), 98 | backref=backref('home_knockout_matches')) 99 | 100 | @declared_attr 101 | def away_team(cls): 102 | return relationship('Clubs', foreign_keys="{}.away_team_id".format(cls.__name__), 103 | backref=backref('away_knockout_matches')) 104 | 105 | 106 | class ClubFriendlyMatches(FriendlyMixin, ClubMatchMixin, ClubSchema, mcm.Matches): 107 | __tablename__ = "club_friendly_matches" 108 | __mapper_args__ = {'polymorphic_identity': 'club_friendly'} 109 | 110 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 111 | 112 | def __repr__(self): 113 | return u"".format( 114 | self.home_team.name, self.away_team.name, self.competition.name, self.date.isoformat() 115 | ).encode('utf-8') 116 | 117 | def __unicode__(self): 118 | return u"".format( 119 | self.home_team.name, self.away_team.name, self.competition.name, self.date.isoformat() 120 | ) 121 | 122 | 123 | class ClubLeagueMatches(LeagueMixin, ClubMatchMixin, ClubSchema, mcm.LeagueMatches, mcm.Matches): 124 | __tablename__ = "club_league_matches" 125 | __mapper_args__ = {'polymorphic_identity': 'league'} 126 | 127 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 128 | 129 | def __repr__(self): 130 | return u"".format( 131 | self.home_team.name, self.away_team.name, self.competition.name, self.matchday, self.date.isoformat() 132 | ).encode('utf-8') 133 | 134 | def __unicode__(self): 135 | return u"".format( 136 | self.home_team.name, self.away_team.name, self.competition.name, self.matchday, self.date.isoformat() 137 | ) 138 | 139 | 140 | class ClubGroupMatches(GroupMixin, ClubMatchMixin, ClubSchema, mcm.GroupMatches, mcm.Matches): 141 | __tablename__ = "club_group_matches" 142 | __mapper_args__ = {'polymorphic_identity': 'club_group'} 143 | 144 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 145 | 146 | def __repr__(self): 147 | return u"".format( 148 | self.home_team.name, self.away_team.name, self.competition.name, self.group_round.value, 149 | self.group, self.matchday, self.date.isoformat() 150 | ).encode('utf-8') 151 | 152 | def __unicode__(self): 153 | return u"".format( 154 | self.home_team.name, self.away_team.name, self.competition.name, self.group_round.value, 155 | self.group, self.matchday, self.date.isoformat() 156 | ) 157 | 158 | 159 | class ClubKnockoutMatches(KnockoutMixin, ClubMatchMixin, ClubSchema, mcm.KnockoutMatches, mcm.Matches): 160 | __tablename__ = "club_knockout_matches" 161 | __mapper_args__ = {'polymorphic_identity': 'club_knockout'} 162 | 163 | id = Column(Integer, ForeignKey('matches.id'), primary_key=True) 164 | 165 | def __repr__(self): 166 | return u"".format( 167 | self.home_team.name, self.away_team.name, self.competition.name, 168 | self.ko_round.value, self.matchday, self.date.isoformat() 169 | ).encode('utf-8') 170 | 171 | def __unicode__(self): 172 | return u"".format( 173 | self.home_team.name, self.away_team.name, self.competition.name, 174 | self.ko_round.value, self.matchday, self.date.isoformat() 175 | ) 176 | 177 | 178 | class ClubMatchLineups(ClubMixin, ClubSchema, mcm.MatchLineups): 179 | __tablename__ = "club_match_lineups" 180 | __mapper_args__ = {'polymorphic_identity': 'club'} 181 | 182 | id = Column(Integer, ForeignKey('lineups.id'), primary_key=True) 183 | 184 | team = relationship('Clubs', foreign_keys="ClubMatchLineups.team_id", backref=backref("lineups")) 185 | 186 | def __repr__(self): 187 | return u"".format( 188 | self.match_id, self.full_name, self.team.name, self.position.name, self.is_starting, self.is_captain 189 | ).encode('utf-8') 190 | 191 | def __unicode__(self): 192 | return u"".format( 193 | self.match_id, self.full_name, self.team.name, self.position.name, self.is_starting, self.is_captain 194 | ) 195 | 196 | 197 | class ClubGoals(ClubMixin, ClubSchema, mce.Goals): 198 | __tablename__ = 'club_goals' 199 | __mapper_args__ = {'polymorphic_identity': 'club'} 200 | 201 | id = Column(Integer, ForeignKey('goals.id'), primary_key=True) 202 | 203 | team = relationship('Clubs', foreign_keys="ClubGoals.team_id", backref=backref("goals")) 204 | 205 | 206 | class ClubMap(ClubSchema): 207 | __tablename__ = "club_mapper" 208 | 209 | id = Column(Integer, ForeignKey('clubs.id'), primary_key=True) 210 | remote_id = Column(Integer, nullable=False, primary_key=True) 211 | supplier_id = Column(Integer, ForeignKey('suppliers.id'), primary_key=True) 212 | 213 | supplier = relationship('Suppliers', backref=backref('clubs')) 214 | 215 | def __repr__(self): 216 | return "".format( 217 | self.id, self.remote_id, self.supplier.name) 218 | -------------------------------------------------------------------------------- /marcotti/models/common/personnel.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, Numeric, String, Sequence, Date, ForeignKey, Unicode 2 | from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method 3 | from sqlalchemy.orm import relationship, backref 4 | from sqlalchemy.schema import CheckConstraint 5 | from sqlalchemy.sql.expression import cast, case 6 | 7 | from models.common import BaseSchema 8 | import models.common.enums as enums 9 | 10 | 11 | class Positions(BaseSchema): 12 | """ 13 | Football player position data model. 14 | """ 15 | __tablename__ = 'positions' 16 | 17 | id = Column(Integer, Sequence('position_id_seq', start=10), primary_key=True) 18 | name = Column(Unicode(20), nullable=False) 19 | type = Column(enums.PositionType.db_type()) 20 | 21 | def __repr__(self): 22 | return u"".format(self.name, self.type.value) 23 | 24 | 25 | class Persons(BaseSchema): 26 | """ 27 | Persons common data model. This model is subclassed by other Personnel data models. 28 | """ 29 | __tablename__ = 'persons' 30 | 31 | person_id = Column(Integer, Sequence('person_id_seq', start=100000), primary_key=True) 32 | first_name = Column(Unicode(40), nullable=False) 33 | middle_name = Column(Unicode(40)) 34 | last_name = Column(Unicode(40), nullable=False) 35 | second_last_name = Column(Unicode(40)) 36 | nick_name = Column(Unicode(40)) 37 | birth_date = Column(Date, nullable=False) 38 | order = Column(enums.NameOrderType.db_type(), default=enums.NameOrderType.western) 39 | type = Column(String) 40 | 41 | country_id = Column(Integer, ForeignKey('countries.id')) 42 | country = relationship('Countries', backref=backref('persons')) 43 | 44 | __mapper_args__ = { 45 | 'polymorphic_identity': 'persons', 46 | 'polymorphic_on': type 47 | } 48 | 49 | @hybrid_property 50 | def full_name(self): 51 | """ 52 | The person's commonly known full name, following naming order conventions. 53 | 54 | If a person has a nickname, that name becomes the person's full name. 55 | 56 | :return: Person's full name. 57 | """ 58 | if self.nick_name is not None: 59 | return self.nick_name 60 | else: 61 | if self.order == enums.NameOrderType.western: 62 | return u"{} {}".format(self.first_name, self.last_name) 63 | elif self.order == enums.NameOrderType.middle: 64 | return u"{} {} {}".format(self.first_name, self.middle_name, self.last_name) 65 | elif self.order == enums.NameOrderType.eastern: 66 | return u"{} {}".format(self.last_name, self.first_name) 67 | 68 | @full_name.expression 69 | def full_name(cls): 70 | """ 71 | The person's commonly known full name, following naming order conventions. 72 | 73 | If a person has a nickname, that name becomes the person's full name. 74 | 75 | :return: Person's full name. 76 | """ 77 | return case( 78 | [(cls.nick_name != None, cls.nick_name)], 79 | else_=case( 80 | [(cls.order == enums.NameOrderType.middle, 81 | cls.first_name + ' ' + cls.middle_name + ' ' + cls.last_name), 82 | (cls.order == enums.NameOrderType.eastern, 83 | cls.last_name + ' ' + cls.first_name)], 84 | else_=cls.first_name + ' ' + cls.last_name 85 | )) 86 | 87 | @hybrid_property 88 | def official_name(self): 89 | """ 90 | The person's legal name, following naming order conventions and with middle names included. 91 | 92 | :return: Person's legal name. 93 | """ 94 | if self.order == enums.NameOrderType.eastern: 95 | return u"{} {}".format(self.last_name, self.first_name) 96 | else: 97 | return u" ".join([getattr(self, field) for field in 98 | ['first_name', 'middle_name', 'last_name', 'second_last_name'] 99 | if getattr(self, field) is not None]) 100 | 101 | @hybrid_method 102 | def exact_age(self, reference): 103 | """ 104 | Player's exact age (years + days) relative to a reference date. 105 | 106 | :param reference: Date object of reference date. 107 | :return: Player's age expressed as a (Year, day) tuple 108 | """ 109 | delta = reference - self.birth_date 110 | years = int(delta.days/365.25) 111 | days = int(delta.days - years*365.25 + 0.5) 112 | return (years, days) 113 | 114 | @hybrid_method 115 | def age(self, reference): 116 | """ 117 | Player's age relative to a reference date. 118 | 119 | :param reference: Date object of reference date. 120 | :return: Integer value of player's age. 121 | """ 122 | delta = reference - self.birth_date 123 | return int(delta.days/365.25) 124 | 125 | @age.expression 126 | def age(cls, reference): 127 | """ 128 | Person's age relative to a reference date. 129 | 130 | :param reference: Date object of reference date. 131 | :return: Integer value of person's age. 132 | """ 133 | return cast((reference - cls.birth_date)/365.25 - 0.5, Integer) 134 | 135 | def __repr__(self): 136 | return u"".format( 137 | self.full_name, self.country.name, self.birth_date.isoformat()).encode('utf-8') 138 | 139 | 140 | class Players(Persons): 141 | """ 142 | Players data model. 143 | 144 | Inherits Persons model. 145 | """ 146 | __tablename__ = 'players' 147 | __mapper_args__ = {'polymorphic_identity': 'players'} 148 | 149 | id = Column(Integer, Sequence('player_id_seq', start=100000), primary_key=True) 150 | person_id = Column(Integer, ForeignKey('persons.person_id')) 151 | 152 | position_id = Column(Integer, ForeignKey('positions.id')) 153 | position = relationship('Positions', backref=backref('players')) 154 | 155 | def __repr__(self): 156 | return u"".format( 157 | self.full_name, self.birth_date.isoformat(), self.country.name, self.position.name).encode('utf-8') 158 | 159 | def __unicode__(self): 160 | return u"".format( 161 | self.full_name, self.birth_date.isoformat(), self.country.name, self.position.name) 162 | 163 | 164 | class PlayerHistory(BaseSchema): 165 | """ 166 | Player physical history data model. 167 | """ 168 | __tablename__ = 'player_histories' 169 | 170 | id = Column(Integer, Sequence('player_hist_id_seq', start=1000000), primary_key=True) 171 | player_id = Column(Integer, ForeignKey('players.id')) 172 | date = Column(Date, doc="Effective date of player physical record") 173 | height = Column(Numeric(3, 2), CheckConstraint('height >= 0 AND height <= 2.50'), nullable=False, 174 | doc="Height of player in meters") 175 | weight = Column(Numeric(3, 0), CheckConstraint('weight >= 0 AND weight <= 150'), nullable=False, 176 | doc="Weight of player in kilograms") 177 | 178 | player = relationship('Players', backref=backref('history')) 179 | 180 | def __repr__(self): 181 | return u"".format( 182 | self.player.full_name, self.date.isoformat(), self.height, self.weight).encode('utf-8') 183 | 184 | def __unicode__(self): 185 | return u"".format( 186 | self.player.full_name, self.date.isoformat(), self.height, self.weight) 187 | 188 | 189 | class Managers(Persons): 190 | """ 191 | Managers data model. 192 | 193 | Inherits Persons model. 194 | """ 195 | __tablename__ = 'managers' 196 | __mapper_args__ = {'polymorphic_identity': 'managers'} 197 | 198 | id = Column(Integer, Sequence('manager_id_seq', start=10000), primary_key=True) 199 | person_id = Column(Integer, ForeignKey('persons.person_id')) 200 | 201 | def __repr__(self): 202 | return u"".format( 203 | self.full_name, self.birth_date.isoformat(), self.country.name).decode('utf-8') 204 | 205 | def __unicode__(self): 206 | return u"".format( 207 | self.full_name, self.birth_date.isoformat(), self.country.name) 208 | 209 | 210 | class Referees(Persons): 211 | """ 212 | Referees data model. 213 | 214 | Inherits Persons model. 215 | """ 216 | __tablename__ = 'referees' 217 | __mapper_args__ = {'polymorphic_identity': 'referees'} 218 | 219 | id = Column(Integer, Sequence('referee_id_seq', start=10000), primary_key=True) 220 | person_id = Column(Integer, ForeignKey('persons.person_id')) 221 | 222 | def __repr__(self): 223 | return u"".format( 224 | self.full_name, self.birth_date.isoformat(), self.country.name).encode('utf-8') 225 | 226 | def __unicode__(self): 227 | return u"".format( 228 | self.full_name, self.birth_date.isoformat(), self.country.name) 229 | -------------------------------------------------------------------------------- /marcotti/models/common/overview.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from sqlalchemy import (case, select, cast, Column, Integer, Numeric, Date, 4 | String, Sequence, ForeignKey, Unicode) 5 | from sqlalchemy.orm import relationship, backref 6 | from sqlalchemy.ext.hybrid import hybrid_property 7 | from sqlalchemy.schema import CheckConstraint 8 | 9 | from models.common import BaseSchema 10 | import models.common.enums as enums 11 | 12 | 13 | class Countries(BaseSchema): 14 | """ 15 | Countries data model. 16 | 17 | Countries are defined as FIFA-affiliated national associations. 18 | """ 19 | __tablename__ = "countries" 20 | 21 | id = Column(Integer, Sequence('country_id_seq', start=100), primary_key=True) 22 | name = Column(Unicode(60)) 23 | code = Column(String(3)) 24 | confederation = Column(enums.ConfederationType.db_type()) 25 | 26 | def __repr__(self): 27 | return u"".format( 28 | self.id, self.name, self.code, self.confederation.value).encode('utf-8') 29 | 30 | 31 | class Years(BaseSchema): 32 | """ 33 | Years data model. 34 | """ 35 | __tablename__ = "years" 36 | 37 | id = Column(Integer, Sequence('year_id_seq', start=100), primary_key=True) 38 | yr = Column(Integer, unique=True) 39 | 40 | def __repr__(self): 41 | return "".format(self.yr) 42 | 43 | 44 | class Seasons(BaseSchema): 45 | """ 46 | Seasons data model. 47 | """ 48 | __tablename__ = "seasons" 49 | 50 | id = Column(Integer, Sequence('season_id_seq', start=100), primary_key=True) 51 | 52 | start_year_id = Column(Integer, ForeignKey('years.id')) 53 | end_year_id = Column(Integer, ForeignKey('years.id')) 54 | 55 | start_year = relationship('Years', foreign_keys=[start_year_id]) 56 | end_year = relationship('Years', foreign_keys=[end_year_id]) 57 | 58 | @hybrid_property 59 | def name(self): 60 | """ 61 | List year(s) that make up season. Seasons over calendar year will be of form YYYY; 62 | seasons over two years will be of form YYYY-YYYY. 63 | """ 64 | if self.start_year.yr == self.end_year.yr: 65 | return self.start_year.yr 66 | else: 67 | return "{0}-{1}".format(self.start_year.yr, self.end_year.yr) 68 | 69 | @name.expression 70 | def name(cls): 71 | """ 72 | List year(s) that make up season. Seasons over calendar year will be of form YYYY; 73 | seasons over two years will be of form YYYY-YYYY. 74 | 75 | This expression allows `name` to be used as a query parameter. 76 | """ 77 | yr1 = select([Years.yr]).where(cls.start_year_id == Years.id).as_scalar() 78 | yr2 = select([Years.yr]).where(cls.end_year_id == Years.id).as_scalar() 79 | return cast(yr1, String) + case([(yr1 == yr2, '')], else_='-'+cast(yr2, String)) 80 | 81 | @hybrid_property 82 | def reference_date(self): 83 | """ 84 | Define the reference date that is used to calculate player ages. 85 | 86 | +------------------------+---------------------+ 87 | | Season type | Reference date | 88 | +========================+=====================+ 89 | | European (Split years) | 30 June | 90 | +------------------------+---------------------+ 91 | | Calendar-year | 31 December | 92 | +------------------------+---------------------+ 93 | 94 | :return: Date object that expresses reference date. 95 | """ 96 | if self.start_year.yr == self.end_year.yr: 97 | return date(self.end_year.yr, 12, 31) 98 | else: 99 | return date(self.end_year.yr, 6, 30) 100 | 101 | def __repr__(self): 102 | return "".format(self.name) 103 | 104 | 105 | class Competitions(BaseSchema): 106 | """ 107 | Competitions common data model. 108 | """ 109 | __tablename__ = 'competitions' 110 | 111 | id = Column(Integer, Sequence('competition_id_seq', start=1000), primary_key=True) 112 | 113 | name = Column(Unicode(80)) 114 | level = Column(Integer) 115 | discriminator = Column('type', String(20)) 116 | 117 | __mapper_args__ = { 118 | 'polymorphic_identity': 'competitions', 119 | 'polymorphic_on': discriminator 120 | } 121 | 122 | 123 | class DomesticCompetitions(Competitions): 124 | """ 125 | Domestic Competitions data model, inherited from Competitions model. 126 | """ 127 | __mapper_args__ = {'polymorphic_identity': 'domestic'} 128 | country_id = Column(Integer, ForeignKey('countries.id')) 129 | country = relationship('Countries', backref=backref('competitions')) 130 | 131 | def __repr__(self): 132 | return u"".format( 133 | self.name, self.country.name, self.level).encode('utf-8') 134 | 135 | 136 | class InternationalCompetitions(Competitions): 137 | """ 138 | International Competitions data model, inherited from Competitions model. 139 | """ 140 | __mapper_args__ = {'polymorphic_identity': 'international'} 141 | 142 | confederation = Column(enums.ConfederationType.db_type()) 143 | 144 | def __repr__(self): 145 | return u"".format( 146 | self.name, self.confederation.value).encode('utf-8') 147 | 148 | 149 | class Venues(BaseSchema): 150 | __tablename__ = 'venues' 151 | 152 | id = Column(Integer, Sequence('venue_id_seq', start=1000), primary_key=True) 153 | 154 | name = Column(Unicode(60), doc="The name of the match venue") 155 | city = Column(Unicode(60), doc="Name of city/locality where venue resides") 156 | region = Column(Unicode(60), doc="Name of administrative region (state, province, etc) where venue resides") 157 | latitude = Column(Numeric(9, 6), CheckConstraint("latitude >= -90.000000 AND latitude <= 90.000000"), 158 | default=0.000000, doc="Venue latitude in decimal degrees") 159 | longitude = Column(Numeric(9, 6), CheckConstraint("longitude >= -180.000000 AND longitude <= 180.000000"), 160 | default=0.000000, doc="Venue longitude in decimal degrees") 161 | altitude = Column(Integer, CheckConstraint("altitude >= -200 AND altitude <= 4500"), 162 | default=0, doc="Venue altitude in meters") 163 | 164 | country_id = Column(Integer, ForeignKey('countries.id')) 165 | country = relationship('Countries', backref=backref('venues')) 166 | timezone_id = Column(Integer, ForeignKey('timezones.id')) 167 | timezone = relationship('Timezones', backref=backref('venues')) 168 | 169 | def __repr__(self): 170 | return u"".format( 171 | self.name, self.city, self.country.name).encode('utf-8') 172 | 173 | 174 | class VenueHistory(BaseSchema): 175 | __tablename__ = 'venue_histories' 176 | 177 | id = Column(Integer, Sequence('venuehist_id_seq', start=10000), primary_key=True) 178 | 179 | date = Column(Date, doc="Effective date of venue configuration") 180 | length = Column(Integer, CheckConstraint("length >= 90 AND length <= 120"), 181 | default=105, doc="Length of venue playing surface in meters") 182 | width = Column(Integer, CheckConstraint("width >= 45 AND width <= 90"), 183 | default=68, doc="Width of venue playing surface in meters") 184 | capacity = Column(Integer, CheckConstraint("capacity >= 0"), 185 | default=0, doc="Total venue capacity (seated and unseated)") 186 | seats = Column(Integer, CheckConstraint("seats >= 0"), 187 | default=0, doc="Total seats at venue") 188 | 189 | venue_id = Column(Integer, ForeignKey('venues.id')) 190 | venue = relationship('Venues', backref=backref('histories')) 191 | surface_id = Column(Integer, ForeignKey('surfaces.id')) 192 | surface = relationship('Surfaces', backref=backref('venues')) 193 | 194 | def __repr__(self): 195 | return u"".format( 196 | self.venue.name, self.date.isoformat(), self.length, self.width, self.capacity).encode('utf-8') 197 | 198 | 199 | class Timezones(BaseSchema): 200 | __tablename__ = 'timezones' 201 | 202 | id = Column(Integer, Sequence('timezone_id_seq', start=1000), primary_key=True) 203 | 204 | name = Column(Unicode(80), doc="Name of the time zone geographic region", nullable=False) 205 | offset = Column(Numeric(4, 2), doc="Offset of the time zone region from UTC, in decimal hours", nullable=False) 206 | confederation = Column(enums.ConfederationType.db_type()) 207 | 208 | def __repr__(self): 209 | return u"".format( 210 | self.name, self.offset, self.confederation.value).encode('utf-8') 211 | 212 | 213 | class Surfaces(BaseSchema): 214 | __tablename__ = 'surfaces' 215 | 216 | id = Column(Integer, Sequence('surface_id_seq', start=10), primary_key=True) 217 | 218 | description = Column(Unicode(60), nullable=False) 219 | type = Column(enums.SurfaceType.db_type()) 220 | 221 | def __repr__(self): 222 | return u"".format( 223 | self.description, self.type.description).encode('utf-8') 224 | -------------------------------------------------------------------------------- /tests/test_club.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import pytest 3 | from sqlalchemy.exc import DataError 4 | 5 | import marcotti.models.club as mc 6 | import marcotti.models.common.overview as mco 7 | import marcotti.models.common.personnel as mcp 8 | import marcotti.models.common.enums as enums 9 | 10 | 11 | club_only = pytest.mark.skipif( 12 | pytest.config.getoption("--schema") != "club", 13 | reason="Test only valid for club databases" 14 | ) 15 | 16 | 17 | @club_only 18 | def test_club_insert(session): 19 | club = mc.Clubs(name=u"Arsenal", 20 | country=mco.Countries(name=u"England", confederation=enums.ConfederationType.europe)) 21 | session.add(club) 22 | 23 | result = session.query(mc.Clubs).one() 24 | assert result.name == u"Arsenal" 25 | assert repr(result) == "" 26 | 27 | 28 | @club_only 29 | def test_club_unicode_insert(session): 30 | club = mc.Clubs(name=u"Фк Спартак Москва", 31 | country=mco.Countries(name=u"Russia", confederation=enums.ConfederationType.europe)) 32 | session.add(club) 33 | 34 | result = session.query(mc.Clubs).join(mco.Countries).filter(mco.Countries.name == u"Russia").one() 35 | 36 | assert result.name == u"Фк Спартак Москва" 37 | assert unicode(result) == u"" 38 | 39 | 40 | @club_only 41 | def test_club_name_overflow(session): 42 | too_long_name = "blahblah" * 8 43 | too_long_club = mc.Clubs(name=too_long_name, 44 | country=mco.Countries(name=u"foo", confederation=enums.ConfederationType.fifa)) 45 | with pytest.raises(DataError): 46 | session.add(too_long_club) 47 | session.commit() 48 | 49 | 50 | @club_only 51 | def test_club_friendly_match_insert(session, club_data): 52 | friendly_match = mc.ClubFriendlyMatches(**club_data) 53 | session.add(friendly_match) 54 | 55 | match_from_db = session.query(mc.ClubFriendlyMatches).one() 56 | 57 | assert match_from_db.season.name == "2014-2015" 58 | assert match_from_db.competition.name == u"Test Competition" 59 | assert match_from_db.competition.country.name == u"England" 60 | assert match_from_db.venue.name == u"Emirates Stadium" 61 | assert match_from_db.home_team.name == u"Arsenal FC" 62 | assert match_from_db.away_team.name == u"Lincoln City FC" 63 | assert match_from_db.home_manager.full_name == u"Arsène Wenger" 64 | assert match_from_db.away_manager.full_name == u"Gary Simpson" 65 | assert match_from_db.referee.full_name == u"Mark Clattenburg" 66 | 67 | 68 | @club_only 69 | def test_club_league_match_insert(session, club_data): 70 | league_match = mc.ClubLeagueMatches(matchday=5, **club_data) 71 | session.add(league_match) 72 | 73 | match_from_db = session.query(mc.ClubLeagueMatches).join(mco.DomesticCompetitions)\ 74 | .filter(mco.DomesticCompetitions.name == club_data['competition'].name).all()[0] 75 | 76 | assert match_from_db.phase == "league" 77 | assert match_from_db.matchday == 5 78 | assert match_from_db.season.name == "2014-2015" 79 | assert match_from_db.competition.name == u"Test Competition" 80 | assert match_from_db.competition.country.name == u"England" 81 | assert match_from_db.venue.name == u"Emirates Stadium" 82 | assert match_from_db.home_team.name == u"Arsenal FC" 83 | assert match_from_db.away_team.name == u"Lincoln City FC" 84 | assert match_from_db.home_manager.full_name == u"Arsène Wenger" 85 | assert match_from_db.away_manager.full_name == u"Gary Simpson" 86 | assert match_from_db.referee.full_name == u"Mark Clattenburg" 87 | 88 | 89 | @club_only 90 | def test_club_group_match_insert(session, club_data): 91 | group_match = mc.ClubGroupMatches( 92 | group_round=enums.GroupRoundType.group_stage, 93 | group='A', 94 | matchday=1, 95 | **club_data) 96 | session.add(group_match) 97 | 98 | match_from_db = session.query(mc.ClubGroupMatches).one() 99 | 100 | assert match_from_db.phase == "group" 101 | assert match_from_db.group_round.value == u"Group Stage" 102 | assert match_from_db.group == "A" 103 | assert match_from_db.matchday == 1 104 | assert match_from_db.season.name == "2014-2015" 105 | assert match_from_db.competition.name == u"Test Competition" 106 | assert match_from_db.competition.country.name == u"England" 107 | assert match_from_db.venue.name == u"Emirates Stadium" 108 | assert match_from_db.home_team.name == u"Arsenal FC" 109 | assert match_from_db.away_team.name == u"Lincoln City FC" 110 | assert match_from_db.home_manager.full_name == u"Arsène Wenger" 111 | assert match_from_db.away_manager.full_name == u"Gary Simpson" 112 | assert match_from_db.referee.full_name == u"Mark Clattenburg" 113 | 114 | 115 | @club_only 116 | def test_club_knockout_match_insert(session, club_data): 117 | knockout_match = mc.ClubKnockoutMatches( 118 | ko_round=enums.KnockoutRoundType.quarterfinal, 119 | **club_data) 120 | session.add(knockout_match) 121 | 122 | match_from_db = session.query(mc.ClubKnockoutMatches).filter( 123 | mc.ClubKnockoutMatches.ko_round == enums.KnockoutRoundType.quarterfinal) 124 | 125 | assert match_from_db[0].phase == "knockout" 126 | assert match_from_db[0].ko_round.value == u"Quarterfinal (1/4)" 127 | assert match_from_db[0].matchday == 1 128 | assert match_from_db[0].extra_time is False 129 | assert match_from_db[0].season.name == "2014-2015" 130 | assert match_from_db[0].competition.name == u"Test Competition" 131 | assert match_from_db[0].competition.country.name == u"England" 132 | assert match_from_db[0].venue.name == u"Emirates Stadium" 133 | assert match_from_db[0].home_team.name == u"Arsenal FC" 134 | assert match_from_db[0].away_team.name == u"Lincoln City FC" 135 | assert match_from_db[0].home_manager.full_name == u"Arsène Wenger" 136 | assert match_from_db[0].away_manager.full_name == u"Gary Simpson" 137 | assert match_from_db[0].referee.full_name == u"Mark Clattenburg" 138 | 139 | 140 | @club_only 141 | def test_club_match_lineup_insert(session, club_data, person_data, position_data): 142 | match = mc.ClubLeagueMatches(matchday=15, **club_data) 143 | session.add(match) 144 | player = mcp.Players(position=position_data[0], **person_data['player'][0]) 145 | session.add(player) 146 | session.commit() 147 | 148 | club_from_db = session.query(mc.Clubs).filter(mc.Clubs.name == u"Arsenal FC").one() 149 | 150 | lineup = mc.ClubMatchLineups( 151 | match_id=match.id, 152 | team_id=club_from_db.id, 153 | player_id=player.id, 154 | position_id=player.position_id 155 | ) 156 | session.add(lineup) 157 | 158 | lineup_from_db = session.query(mc.ClubMatchLineups).join(mc.ClubLeagueMatches)\ 159 | .filter(mc.ClubLeagueMatches.id == match.id) 160 | 161 | assert lineup_from_db.count() == 1 162 | assert unicode(lineup_from_db[0]) == u"".format(match.id) 164 | 165 | 166 | @club_only 167 | def test_club_goal_insert(session, club_data, person_data, position_data): 168 | match = mc.ClubLeagueMatches(matchday=15, **club_data) 169 | session.add(match) 170 | player = mcp.Players(position=position_data[0], **person_data['player'][0]) 171 | session.add(player) 172 | session.commit() 173 | 174 | club_from_db = session.query(mc.Clubs).filter(mc.Clubs.name == u"Arsenal FC").one() 175 | 176 | lineup = mc.ClubMatchLineups( 177 | match_id=match.id, 178 | team_id=club_from_db.id, 179 | player_id=player.id, 180 | position_id=player.position_id 181 | ) 182 | session.add(lineup) 183 | session.commit() 184 | 185 | goal = mc.ClubGoals( 186 | lineup_id=lineup.id, 187 | team_id=club_from_db.id, 188 | bodypart=enums.BodypartType.head, 189 | event=enums.ShotEventType.cross_ck, 190 | time=70 191 | ) 192 | session.add(goal) 193 | 194 | goals_from_db = session.query(mc.ClubGoals).join(mc.ClubMatchLineups).join(mc.ClubLeagueMatches).filter( 195 | mc.ClubLeagueMatches.id == match.id 196 | ) 197 | 198 | assert goals_from_db.count() == 1 199 | assert goals_from_db[0].team.name == u"Arsenal FC" 200 | assert goals_from_db[0].lineup.full_name == u"Miguel Ángel Ponce" 201 | assert goals_from_db[0].bodypart.value == "Head" 202 | assert goals_from_db[0].event.value == "Cross from corner kick" 203 | assert goals_from_db[0].time == 70 204 | 205 | 206 | @club_only 207 | def test_club_penalty_shootout_opener_insert(session, club_data): 208 | match = mc.ClubKnockoutMatches(ko_round=enums.KnockoutRoundType.semifinal, **club_data) 209 | session.add(match) 210 | session.commit() 211 | 212 | result = session.query(mc.ClubKnockoutMatches.home_team_id, mc.ClubKnockoutMatches.away_team_id)\ 213 | .filter_by(id=match.id) 214 | 215 | home, away = result[0] 216 | 217 | shootout = mc.ClubPenaltyShootoutOpeners(match_id=match.id, team_id=home) 218 | session.add(shootout) 219 | 220 | shootout_from_db = session.query(mc.ClubPenaltyShootoutOpeners)\ 221 | .filter(mc.ClubPenaltyShootoutOpeners.match_id == match.id).one() 222 | 223 | assert unicode(shootout_from_db) == u"".format(match.id) 224 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # data fixtures for functional tests 3 | 4 | from datetime import date, time 5 | 6 | import pytest 7 | 8 | import marcotti.models.common.enums as enums 9 | import marcotti.models.common.overview as mco 10 | import marcotti.models.common.personnel as mcp 11 | import marcotti.models.club as mc 12 | 13 | 14 | @pytest.fixture 15 | def comp_data(): 16 | return { 17 | 'domestic': { 18 | 'name': u"English Premier League", 19 | 'level': 1, 20 | 'country': mco.Countries(name=u"England", confederation=enums.ConfederationType.europe) 21 | }, 22 | 'international': { 23 | 'name': u"FIFA Club World Cup", 24 | 'level': 1, 25 | 'confederation': enums.ConfederationType.europe 26 | } 27 | } 28 | 29 | 30 | @pytest.fixture 31 | def season_data(): 32 | return { 33 | 'start_year': { 34 | 'yr': 2012 35 | }, 36 | 'end_year': { 37 | 'yr': 2013 38 | } 39 | } 40 | 41 | 42 | @pytest.fixture 43 | def venue_data(): 44 | england = mco.Countries(name=u"England", confederation=enums.ConfederationType.europe) 45 | tz_london = mco.Timezones(name=u"Europe/London", offset=0.0, confederation=enums.ConfederationType.europe) 46 | return { 47 | "name": u"Emirates Stadium", 48 | "city": u"London", 49 | "country": england, 50 | "timezone": tz_london, 51 | "latitude": 51.555000, 52 | "longitude": -0.108611, 53 | "altitude": 41 54 | } 55 | 56 | 57 | @pytest.fixture 58 | def venue_config(): 59 | return { 60 | "date": date(2006, 7, 22), 61 | "length": 105, 62 | "width": 68, 63 | "capacity": 60361, 64 | "seats": 60361, 65 | "surface": mco.Surfaces(description=u"Desso GrassMaster", type=enums.SurfaceType.hybrid) 66 | } 67 | 68 | 69 | @pytest.fixture 70 | def person_data(): 71 | return { 72 | 'generic': { 73 | 'first_name': u"John", 74 | 'last_name': u"Doe", 75 | 'birth_date': date(1980, 1, 1), 76 | 'country': mco.Countries(name=u"Portlandia", confederation=enums.ConfederationType.north_america) 77 | }, 78 | 'manager': [ 79 | { 80 | 'first_name': u"Arsène", 81 | 'last_name': u"Wenger", 82 | 'birth_date': date(1949, 10, 22), 83 | 'country': mco.Countries(name=u"France", confederation=enums.ConfederationType.europe) 84 | }, 85 | { 86 | 'first_name': u"Arthur", 87 | 'middle_name': u"Antunes", 88 | 'last_name': u"Coimbra", 89 | 'nick_name': u"Zico", 90 | 'birth_date': date(1953, 3, 3), 91 | 'country': mco.Countries(name=u"Brazil", confederation=enums.ConfederationType.south_america) 92 | } 93 | ], 94 | 'player': [ 95 | { 96 | 'first_name': u'Miguel', 97 | 'middle_name': u'Ángel', 98 | 'last_name': u'Ponce', 99 | 'second_last_name': u'Briseño', 100 | 'birth_date': date(1989, 4, 12), 101 | 'country': mco.Countries(name=u"Mexico", confederation=enums.ConfederationType.north_america), 102 | 'order': enums.NameOrderType.middle 103 | }, 104 | { 105 | 'first_name': u"Cristiano", 106 | 'middle_name': u"Ronaldo", 107 | 'last_name': u"Aveiro", 108 | 'second_last_name': u"dos Santos", 109 | 'nick_name': u"Cristiano Ronaldo", 110 | 'birth_date': date(1985, 2, 5), 111 | 'country': mco.Countries(name=u"Portugal", confederation=enums.ConfederationType.europe), 112 | 'order': enums.NameOrderType.western 113 | }, 114 | { 115 | 'first_name': u'Heung-Min', 116 | 'last_name': u'Son', 117 | 'birth_date': date(1992, 7, 8), 118 | 'country': mco.Countries(name=u"Korea Republic", confederation=enums.ConfederationType.asia), 119 | 'order': enums.NameOrderType.eastern 120 | } 121 | ], 122 | 'referee': [ 123 | { 124 | 'first_name': u"Christopher", 125 | 'middle_name': u"J", 126 | 'last_name': u"Foy", 127 | 'birth_date': date(1962, 11, 20), 128 | 'country': mco.Countries(name=u"England", confederation=enums.ConfederationType.europe) 129 | }, 130 | { 131 | 'first_name': u"Cüneyt", 132 | 'last_name': u"Çakır", 133 | 'birth_date': date(1976, 11, 23), 134 | 'country': mco.Countries(name=u"Turkey", confederation=enums.ConfederationType.europe) 135 | } 136 | ] 137 | } 138 | 139 | 140 | @pytest.fixture 141 | def position_data(): 142 | return [ 143 | mcp.Positions(name=u"Left back", type=enums.PositionType.defender), 144 | mcp.Positions(name=u"Forward", type=enums.PositionType.forward), 145 | mcp.Positions(name=u"Second striker", type=enums.PositionType.forward) 146 | ] 147 | 148 | 149 | @pytest.fixture 150 | def player_history_data(): 151 | return [ 152 | { 153 | 'date': date(1996, 1, 1), 154 | 'height': 1.70, 155 | 'weight': 70 156 | }, 157 | { 158 | 'date': date(1998, 7, 15), 159 | 'height': 1.74, 160 | 'weight': 76 161 | }, 162 | { 163 | 'date': date(2001, 3, 11), 164 | 'height': 1.76, 165 | 'weight': 80 166 | } 167 | ] 168 | 169 | 170 | @pytest.fixture 171 | def match_condition_data(): 172 | return { 173 | 'kickoff_time': time(19, 30), 174 | 'kickoff_temp': 15.0, 175 | 'kickoff_humidity': 68.0, 176 | 'kickoff_weather': enums.WeatherConditionType.partly_cloudy, 177 | 'halftime_weather': enums.WeatherConditionType.clear, 178 | 'fulltime_weather': enums.WeatherConditionType.windy_clear 179 | } 180 | 181 | 182 | @pytest.fixture 183 | def match_data(comp_data, season_data, venue_data, person_data): 184 | return { 185 | "date": date(2012, 12, 12), 186 | "competition": mco.DomesticCompetitions(**comp_data['domestic']), 187 | "season": mco.Seasons(**{k: mco.Years(**v) for k, v in season_data.items()}), 188 | "venue": mco.Venues(**venue_data), 189 | "home_manager": mcp.Managers(**person_data['manager'][0]), 190 | "away_manager": mcp.Managers(**person_data['manager'][1]), 191 | "referee": mcp.Referees(**person_data['referee'][0]) 192 | } 193 | 194 | 195 | @pytest.fixture 196 | def club_data(): 197 | england = mco.Countries(name=u"England", confederation=enums.ConfederationType.europe) 198 | france = mco.Countries(name=u"France", confederation=enums.ConfederationType.europe) 199 | tz_london = mco.Timezones(name=u"Europe/London", offset=0.0, confederation=enums.ConfederationType.europe) 200 | return { 201 | 'date': date(2015, 1, 1), 202 | 'competition': mco.DomesticCompetitions(name=u'Test Competition', level=1, country=england), 203 | 'season': mco.Seasons(start_year=mco.Years(yr=2014), end_year=mco.Years(yr=2015)), 204 | 'venue': mco.Venues(name=u"Emirates Stadium", city=u"London", country=england, timezone=tz_london), 205 | 'home_team': mc.Clubs(name=u"Arsenal FC", country=england), 206 | 'away_team': mc.Clubs(name=u"Lincoln City FC", country=england), 207 | 'home_manager': mcp.Managers(first_name=u"Arsène", last_name=u"Wenger", 208 | birth_date=date(1949, 10, 22), country=france), 209 | 'away_manager': mcp.Managers(first_name=u"Gary", last_name=u"Simpson", 210 | birth_date=date(1961, 4, 11), country=england), 211 | 'referee': mcp.Referees(first_name=u"Mark", last_name=u"Clattenburg", 212 | birth_date=date(1975, 3, 13), country=england) 213 | } 214 | 215 | 216 | @pytest.fixture 217 | def national_data(): 218 | mexico = mco.Countries(name=u"Mexico", confederation=enums.ConfederationType.north_america) 219 | england = mco.Countries(name=u"England", confederation=enums.ConfederationType.europe) 220 | france = mco.Countries(name=u"France", confederation=enums.ConfederationType.europe) 221 | italy = mco.Countries(name=u"Italy", confederation=enums.ConfederationType.europe) 222 | tz_london = mco.Timezones(name=u"Europe/London", offset=0.0, confederation=enums.ConfederationType.europe) 223 | return { 224 | 'date': date(1997, 11, 12), 225 | 'competition': mco.InternationalCompetitions(name=u"International Cup", level=1, 226 | confederation=enums.ConfederationType.fifa), 227 | 'season': mco.Seasons(start_year=mco.Years(yr=1997), end_year=mco.Years(yr=1998)), 228 | 'venue': mco.Venues(name=u"Emirates Stadium", city=u"London", country=england, timezone=tz_london), 229 | 'home_team': france, 230 | 'away_team': mexico, 231 | 'home_manager': mcp.Managers(first_name=u"Arsène", last_name=u"Wenger", 232 | birth_date=date(1949, 10, 22), country=france), 233 | 'away_manager': mcp.Managers(first_name=u"Gary", last_name=u"Simpson", 234 | birth_date=date(1961, 4, 11), country=england), 235 | 'referee': mcp.Referees(first_name=u"Pierluigi", last_name=u"Collina", 236 | birth_date=date(1960, 2, 13), country=italy) 237 | } 238 | -------------------------------------------------------------------------------- /tests/test_personnel.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from datetime import date 3 | 4 | import pytest 5 | from sqlalchemy.exc import DataError, IntegrityError 6 | 7 | import marcotti.models.common.enums as enums 8 | import marcotti.models.common.overview as mco 9 | import marcotti.models.common.personnel as mcp 10 | 11 | 12 | def test_person_generic_insert(session, person_data): 13 | """Person 001: Insert generic personnel data into Persons model and verify data.""" 14 | generic_person = mcp.Persons(**person_data['generic']) 15 | session.add(generic_person) 16 | record = session.query(mcp.Persons).one() 17 | assert record.full_name == u"John Doe" 18 | assert record.official_name == u"John Doe" 19 | assert record.age(date(2010, 1, 1)) == 30 20 | assert record.exact_age(date(2010, 3, 15)) == (30, 74) 21 | assert repr(record) == u"" 22 | 23 | 24 | def test_person_middle_naming_order(session, person_data): 25 | """Person 002: Return correct format of Person's name with Western Middle naming order.""" 26 | persons = [mcp.Persons(**data) for key, records in person_data.items() 27 | for data in records if key in ['player', 'manager', 'referee']] 28 | session.add_all(persons) 29 | 30 | person_from_db = session.query(mcp.Persons).filter(mcp.Persons.order == enums.NameOrderType.middle).one() 31 | assert person_from_db.full_name == u"Miguel Ángel Ponce" 32 | assert person_from_db.official_name == u"Miguel Ángel Ponce Briseño" 33 | 34 | 35 | def test_person_eastern_naming_order(session, person_data): 36 | """Person 003: Return correct format of Person's name with Eastern naming order.""" 37 | persons = [mcp.Persons(**data) for key, records in person_data.items() 38 | for data in records if key in ['player', 'manager', 'referee']] 39 | session.add_all(persons) 40 | 41 | person_from_db = session.query(mcp.Persons).filter(mcp.Persons.order == enums.NameOrderType.eastern).one() 42 | assert person_from_db.full_name == u"Son Heung-Min" 43 | assert person_from_db.full_name == person_from_db.official_name 44 | 45 | 46 | def test_person_nickname(session, person_data): 47 | """Person 004: Return correct name of Person with nickname.""" 48 | persons = [mcp.Persons(**data) for key, records in person_data.items() 49 | for data in records if key in ['player'] and 'nick_name' in data] 50 | session.add_all(persons) 51 | 52 | person_from_db = session.query(mcp.Persons).one() 53 | 54 | assert person_from_db.full_name == u"Cristiano Ronaldo" 55 | assert person_from_db.official_name == u"Cristiano Ronaldo Aveiro dos Santos" 56 | 57 | 58 | def test_person_missing_first_name_error(session, person_data): 59 | """Person 005: Verify error if first name is missing from Persons data.""" 60 | generic_data_without_first = {key: value for key, value in person_data['generic'].items() if key != 'first_name'} 61 | generic_person = mcp.Persons(**generic_data_without_first) 62 | with pytest.raises(IntegrityError): 63 | session.add(generic_person) 64 | session.commit() 65 | 66 | 67 | def test_person_missing_last_name_error(session, person_data): 68 | """Person 006: Verify error if last name is missing from Persons data.""" 69 | generic_data_without_last = {key: value for key, value in person_data['generic'].items() if key != 'last_name'} 70 | generic_person = mcp.Persons(**generic_data_without_last) 71 | with pytest.raises(IntegrityError): 72 | session.add(generic_person) 73 | session.commit() 74 | 75 | 76 | def test_person_missing_birth_date_error(session, person_data): 77 | """Person 007: Verify error if birth date is missing from Persons data.""" 78 | generic_data_without_dob = {key: value for key, value in person_data['generic'].items() if key != 'birth_date'} 79 | generic_person = mcp.Persons(**generic_data_without_dob) 80 | with pytest.raises(IntegrityError): 81 | session.add(generic_person) 82 | session.commit() 83 | 84 | 85 | def test_person_age_query(session, person_data): 86 | """Person 008: Verify record retrieved when Persons is queried for matching ages.""" 87 | reference_date = date(2015, 7, 1) 88 | persons = [mcp.Persons(**data) for key, records in person_data.items() 89 | for data in records if key in ['player', 'manager', 'referee']] 90 | session.add_all(persons) 91 | records = session.query(mcp.Persons).filter(mcp.Persons.age(reference_date) == 22) 92 | 93 | assert records.count() == 1 94 | 95 | son_hm = records.all()[0] 96 | assert son_hm.age(reference_date) == 22 97 | assert son_hm.exact_age(reference_date) == (22, 358) 98 | 99 | 100 | def test_position_insert(session): 101 | """Positions 001: Insert generic data into Positions model and verify data.""" 102 | left_fb = mcp.Positions(name=u"Left full-back", type=enums.PositionType.defender) 103 | session.add(left_fb) 104 | 105 | position_from_db = session.query(mcp.Positions).one() 106 | assert repr(position_from_db) == u"" 107 | 108 | 109 | def test_position_blank_error(session): 110 | """Positions 002: Verify error if blank name in Positions model.""" 111 | left_fb = mcp.Positions(type=enums.PositionType.defender) 112 | with pytest.raises(IntegrityError): 113 | session.add(left_fb) 114 | session.commit() 115 | 116 | 117 | def test_player_insert(session, person_data, position_data): 118 | player_data = [data for key, records in person_data.items() for data in records if key in ['player']] 119 | for player, position in zip(player_data, position_data): 120 | player['position'] = position 121 | players = [mcp.Players(**data) for data in player_data] 122 | session.add_all(players) 123 | 124 | players_from_db = session.query(mcp.Players) 125 | assert players_from_db.count() == len(players) 126 | 127 | 128 | def test_player_representation(session, person_data, position_data): 129 | player_data = [data for key, records in person_data.items() for data in records if key in ['player']] 130 | for player, position in zip(player_data, position_data): 131 | player['position'] = position 132 | players = [mcp.Players(**data) for data in player_data] 133 | session.add_all(players) 134 | 135 | korean_player = session.query(mcp.Players).join(mco.Countries).\ 136 | filter(mco.Countries.name == u"Korea Republic").one() 137 | assert repr(korean_player) == u"" 139 | 140 | ronaldo = session.query(mcp.Players).filter(mcp.Players.nick_name == u"Cristiano Ronaldo").one() 141 | assert repr(ronaldo) == u"" 143 | 144 | mexican_player = session.query(mcp.Players).join(mco.Countries).filter(mco.Countries.name == u"Mexico").one() 145 | assert unicode(mexican_player) == u"" 147 | 148 | 149 | def test_manager_insert(session, person_data): 150 | manager_data = [mcp.Managers(**data) for key, records in person_data.items() 151 | for data in records if key in ['manager']] 152 | session.add_all(manager_data) 153 | 154 | managers_from_db = session.query(mcp.Managers) 155 | assert managers_from_db.count() == len(manager_data) 156 | 157 | persons_from_db = session.query(mcp.Persons) 158 | assert persons_from_db.count() == managers_from_db.count() 159 | 160 | 161 | def test_manager_representation(session, person_data): 162 | manager_data = [mcp.Managers(**data) for key, records in person_data.items() 163 | for data in records if key in ['manager']] 164 | session.add_all(manager_data) 165 | 166 | zico = session.query(mcp.Managers).filter(mcp.Managers.full_name == u"Zico").one() 167 | assert repr(zico) == u"" 168 | 169 | wenger = session.query(mcp.Managers).filter(mcp.Managers.last_name == u"Wenger").one() 170 | assert unicode(wenger) == u"" 171 | 172 | 173 | def test_referee_insert(session, person_data): 174 | referees = [mcp.Referees(**data) for key, records in person_data.items() 175 | for data in records if key in ['referees']] 176 | session.add_all(referees) 177 | 178 | referees_from_db = session.query(mcp.Referees) 179 | assert referees_from_db.count() == len(referees) 180 | 181 | persons_from_db = session.query(mcp.Persons) 182 | assert persons_from_db.count() == referees_from_db.count() 183 | 184 | 185 | def test_referee_representation(session, person_data): 186 | referees = [mcp.Referees(**data) for key, records in person_data.items() 187 | for data in records if key in ['referee']] 188 | session.add_all(referees) 189 | 190 | english_referee = session.query(mcp.Referees).join(mco.Countries).filter(mco.Countries.name == u"England").one() 191 | assert unicode(english_referee) == u"" 192 | 193 | turkish_referee = session.query(mcp.Referees).join(mco.Countries).filter(mco.Countries.name == u"Turkey").one() 194 | assert unicode(turkish_referee) == u"" 195 | 196 | 197 | def test_player_history_insert(session, person_data, player_history_data): 198 | player_data = dict(position=mcp.Positions(name=u'Central Midfielder', type=enums.PositionType.midfielder), 199 | **person_data['generic']) 200 | generic_player = mcp.Players(**player_data) 201 | player_history = [mcp.PlayerHistory(**dict(player=generic_player, **data)) for data in player_history_data] 202 | session.add_all(player_history) 203 | 204 | history_from_db = session.query(mcp.PlayerHistory).join(mcp.Players).\ 205 | filter(mcp.Players.last_name == u"Doe") 206 | 207 | assert history_from_db.count() == len(player_history) 208 | 209 | 210 | def test_player_history_representation(session, person_data, player_history_data): 211 | player_data = dict(position=mcp.Positions(name=u'Central Midfielder', type=enums.PositionType.midfielder), 212 | **person_data['generic']) 213 | generic_player = mcp.Players(**player_data) 214 | player_history = [mcp.PlayerHistory(**dict(player=generic_player, **data)) for data in player_history_data] 215 | session.add_all(player_history) 216 | 217 | history_from_db = session.query(mcp.PlayerHistory).join(mcp.Players).\ 218 | filter(mcp.Players.last_name == u"Doe", mcp.PlayerHistory.date == date(1998, 7, 15)).one() 219 | 220 | assert repr(history_from_db) == u"" 221 | -------------------------------------------------------------------------------- /marcotti/data/timezones.csv: -------------------------------------------------------------------------------- 1 | name,confederation,offset 2 | Pacific/Pago_Pago,OFC,-11 3 | Pacific/Niue,OFC,-11 4 | Pacific/Midway,OFC,-11 5 | Pacific/Apia,OFC,-11 6 | America/Adak,CONCACAF,-10 7 | Pacific/Honolulu,CONCACAF,-10 8 | Pacific/Rarotonga,OFC,-10 9 | Pacific/Tahiti,OFC,-10 10 | Pacific/Fakaofo,OFC,-10 11 | Pacific/Johnston,OFC,-10 12 | Pacific/Marquesas,OFC,-9.5 13 | America/Anchorage,CONCACAF,-9 14 | America/Juneau,CONCACAF,-9 15 | America/Yakutat,CONCACAF,-9 16 | America/Nome,CONCACAF,-9 17 | Pacific/Gambier,OFC,-9 18 | America/Vancouver,CONCACAF,-8 19 | America/Whitehorse,CONCACAF,-8 20 | America/Dawson,CONCACAF,-8 21 | America/Tijuana,CONCACAF,-8 22 | America/Santa_Isabel,CONCACAF,-8 23 | America/Los_Angeles,CONCACAF,-8 24 | America/Edmonton,CONCACAF,-7 25 | America/Cambridge_Bay,CONCACAF,-7 26 | America/Yellowknife,CONCACAF,-7 27 | America/Inuvik,CONCACAF,-7 28 | America/Dawson_Creek,CONCACAF,-7 29 | America/Mazatlan,CONCACAF,-7 30 | America/Chihuahua,CONCACAF,-7 31 | America/Ojinaga,CONCACAF,-7 32 | America/Hermosillo,CONCACAF,-7 33 | America/Denver,CONCACAF,-7 34 | America/Boise,CONCACAF,-7 35 | America/Shiprock,CONCACAF,-7 36 | America/Phoenix,CONCACAF,-7 37 | America/Belize,CONCACAF,-6 38 | America/Rankin_Inlet,CONCACAF,-6 39 | America/Winnipeg,CONCACAF,-6 40 | America/Rainy_River,CONCACAF,-6 41 | America/Regina,CONCACAF,-6 42 | America/Swift_Current,CONCACAF,-6 43 | America/Costa_Rica,CONCACAF,-6 44 | America/Guatemala,CONCACAF,-6 45 | America/Tegucigalpa,CONCACAF,-6 46 | America/Mexico_City,CONCACAF,-6 47 | America/Cancun,CONCACAF,-6 48 | America/Merida,CONCACAF,-6 49 | America/Monterrey,CONCACAF,-6 50 | America/Matamoros,CONCACAF,-6 51 | America/Managua,CONCACAF,-6 52 | America/El_Salvador,CONCACAF,-6 53 | America/Chicago,CONCACAF,-6 54 | America/Indiana/Tell_City,CONCACAF,-6 55 | America/Indiana/Knox,CONCACAF,-6 56 | America/Menominee,CONCACAF,-6 57 | America/North_Dakota/Center,CONCACAF,-6 58 | America/North_Dakota/New_Salem,CONCACAF,-6 59 | Pacific/Easter,CONMEBOL,-6 60 | Pacific/Galapagos,CONMEBOL,-6 61 | America/Nassau,CONCACAF,-5 62 | America/Montreal,CONCACAF,-5 63 | America/Toronto,CONCACAF,-5 64 | America/Nipigon,CONCACAF,-5 65 | America/Thunder_Bay,CONCACAF,-5 66 | America/Iqaluit,CONCACAF,-5 67 | America/Pangnirtung,CONCACAF,-5 68 | America/Resolute,CONCACAF,-5 69 | America/Atikokan,CONCACAF,-5 70 | America/Havana,CONCACAF,-5 71 | America/Port-au-Prince,CONCACAF,-5 72 | America/Jamaica,CONCACAF,-5 73 | America/Cayman,CONCACAF,-5 74 | America/Panama,CONCACAF,-5 75 | America/Lima,CONCACAF,-5 76 | America/Grand_Turk,CONCACAF,-5 77 | America/New_York,CONCACAF,-5 78 | America/Detroit,CONCACAF,-5 79 | America/Kentucky/Louisville,CONCACAF,-5 80 | America/Kentucky/Monticello,CONCACAF,-5 81 | America/Indiana/Indianapolis,CONCACAF,-5 82 | America/Indiana/Vincennes,CONCACAF,-5 83 | America/Indiana/Winamac,CONCACAF,-5 84 | America/Indiana/Marengo,CONCACAF,-5 85 | America/Indiana/Petersburg,CONCACAF,-5 86 | America/Indiana/Vevay,CONCACAF,-5 87 | America/Bogota,CONMEBOL,-5 88 | America/Guayaquil,CONMEBOL,-5 89 | America/Caracas,CONMEBOL,-4.5 90 | America/Antigua,CONCACAF,-4 91 | America/Anguilla,CONCACAF,-4 92 | America/Curacao,CONCACAF,-4 93 | America/Aruba,CONCACAF,-4 94 | America/Barbados,CONCACAF,-4 95 | America/St_Barthelemy,CONCACAF,-4 96 | Atlantic/Bermuda,CONCACAF,-4 97 | America/Halifax,CONCACAF,-4 98 | America/Glace_Bay,CONCACAF,-4 99 | America/Moncton,CONCACAF,-4 100 | America/Goose_Bay,CONCACAF,-4 101 | America/Blanc-Sablon,CONCACAF,-4 102 | America/Dominica,CONCACAF,-4 103 | America/Santo_Domingo,CONCACAF,-4 104 | America/Grenada,CONCACAF,-4 105 | America/Guadeloupe,CONCACAF,-4 106 | America/Guyana,CONCACAF,-4 107 | America/St_Kitts,CONCACAF,-4 108 | America/St_Lucia,CONCACAF,-4 109 | America/Marigot,CONCACAF,-4 110 | America/Martinique,CONCACAF,-4 111 | America/Montserrat,CONCACAF,-4 112 | America/Puerto_Rico,CONCACAF,-4 113 | America/Port_of_Spain,CONCACAF,-4 114 | America/St_Vincent,CONCACAF,-4 115 | America/Tortola,CONCACAF,-4 116 | America/St_Thomas,CONCACAF,-4 117 | America/Argentina/San_Luis,CONMEBOL,-4 118 | America/La_Paz,CONMEBOL,-4 119 | America/Campo_Grande,CONMEBOL,-4 120 | America/Cuiaba,CONMEBOL,-4 121 | America/Porto_Velho,CONMEBOL,-4 122 | America/Boa_Vista,CONMEBOL,-4 123 | America/Manaus,CONMEBOL,-4 124 | America/Eirunepe,CONMEBOL,-4 125 | America/Rio_Branco,CONMEBOL,-4 126 | America/Santiago,CONMEBOL,-4 127 | America/Asuncion,CONMEBOL,-4 128 | America/St_Johns,CONCACAF,-3.5 129 | America/Cayenne,CONCACAF,-3 130 | America/Miquelon,CONCACAF,-3 131 | America/Paramaribo,CONCACAF,-3 132 | America/Argentina/Buenos_Aires,CONMEBOL,-3 133 | America/Argentina/Cordoba,CONMEBOL,-3 134 | America/Argentina/Salta,CONMEBOL,-3 135 | America/Argentina/Jujuy,CONMEBOL,-3 136 | America/Argentina/Tucuman,CONMEBOL,-3 137 | America/Argentina/Catamarca,CONMEBOL,-3 138 | America/Argentina/La_Rioja,CONMEBOL,-3 139 | America/Argentina/San_Juan,CONMEBOL,-3 140 | America/Argentina/Mendoza,CONMEBOL,-3 141 | America/Argentina/Rio_Gallegos,CONMEBOL,-3 142 | America/Argentina/Ushuaia,CONMEBOL,-3 143 | America/Belem,CONMEBOL,-3 144 | America/Fortaleza,CONMEBOL,-3 145 | America/Recife,CONMEBOL,-3 146 | America/Araguaina,CONMEBOL,-3 147 | America/Maceio,CONMEBOL,-3 148 | America/Bahia,CONMEBOL,-3 149 | America/Sao_Paulo,CONMEBOL,-3 150 | America/Santarem,CONMEBOL,-3 151 | America/Montevideo,CONMEBOL,-3 152 | America/Noronha,CONMEBOL,-2 153 | Atlantic/Cape_Verde,CAF,-1 154 | Atlantic/Azores,UEFA,-1 155 | Africa/Ouagadougou,CAF,0 156 | Africa/Abidjan,CAF,0 157 | Africa/El_Aaiun,CAF,0 158 | Africa/Accra,CAF,0 159 | Africa/Banjul,CAF,0 160 | Africa/Conakry,CAF,0 161 | Africa/Bissau,CAF,0 162 | Africa/Monrovia,CAF,0 163 | Africa/Casablanca,CAF,0 164 | Africa/Bamako,CAF,0 165 | Africa/Nouakchott,CAF,0 166 | Africa/Freetown,CAF,0 167 | Africa/Dakar,CAF,0 168 | Africa/Sao_Tome,CAF,0 169 | Africa/Lome,CAF,0 170 | Atlantic/Canary,UEFA,0 171 | Atlantic/Faroe,UEFA,0 172 | Europe/London,UEFA,0 173 | Europe/Guernsey,UEFA,0 174 | Europe/Dublin,UEFA,0 175 | Atlantic/Reykjavik,UEFA,0 176 | Europe/Lisbon,UEFA,0 177 | Atlantic/Madeira,UEFA,0 178 | Africa/Luanda,CAF,1 179 | Africa/Porto-Novo,CAF,1 180 | Africa/Kinshasa,CAF,1 181 | Africa/Bangui,CAF,1 182 | Africa/Brazzaville,CAF,1 183 | Africa/Douala,CAF,1 184 | Africa/Algiers,CAF,1 185 | Africa/Ceuta,CAF,1 186 | Africa/Libreville,CAF,1 187 | Africa/Malabo,CAF,1 188 | Africa/Windhoek,CAF,1 189 | Africa/Niamey,CAF,1 190 | Africa/Lagos,CAF,1 191 | Africa/Ndjamena,CAF,1 192 | Africa/Tunis,CAF,1 193 | Europe/Andorra,UEFA,1 194 | Europe/Tirane,UEFA,1 195 | Europe/Vienna,UEFA,1 196 | Europe/Sarajevo,UEFA,1 197 | Europe/Brussels,UEFA,1 198 | Europe/Zurich,UEFA,1 199 | Europe/Prague,UEFA,1 200 | Europe/Berlin,UEFA,1 201 | Europe/Copenhagen,UEFA,1 202 | Europe/Madrid,UEFA,1 203 | Europe/Paris,UEFA,1 204 | Europe/Gibraltar,UEFA,1 205 | Europe/Zagreb,UEFA,1 206 | Europe/Budapest,UEFA,1 207 | Europe/Rome,UEFA,1 208 | Europe/Vaduz,UEFA,1 209 | Europe/Luxembourg,UEFA,1 210 | Europe/Monaco,UEFA,1 211 | Europe/Podgorica,UEFA,1 212 | Europe/Skopje,UEFA,1 213 | Europe/Malta,UEFA,1 214 | Europe/Amsterdam,UEFA,1 215 | Europe/Oslo,UEFA,1 216 | Europe/Warsaw,UEFA,1 217 | Europe/Belgrade,UEFA,1 218 | Europe/Stockholm,UEFA,1 219 | Europe/Ljubljana,UEFA,1 220 | Europe/Bratislava,UEFA,1 221 | Europe/San_Marino,UEFA,1 222 | Asia/Amman,AFC,2 223 | Asia/Beirut,AFC,2 224 | Asia/Gaza,AFC,2 225 | Asia/Damascus,AFC,2 226 | Africa/Bujumbura,CAF,2 227 | Africa/Gaborone,CAF,2 228 | Africa/Lubumbashi,CAF,2 229 | Africa/Cairo,CAF,2 230 | Africa/Maseru,CAF,2 231 | Africa/Tripoli,CAF,2 232 | Africa/Blantyre,CAF,2 233 | Africa/Maputo,CAF,2 234 | Africa/Kigali,CAF,2 235 | Africa/Mbabane,CAF,2 236 | Africa/Johannesburg,CAF,2 237 | Africa/Lusaka,CAF,2 238 | Africa/Harare,CAF,2 239 | Europe/Mariehamn,UEFA,2 240 | Europe/Sofia,UEFA,2 241 | Europe/Minsk,UEFA,2 242 | Asia/Nicosia,UEFA,2 243 | Europe/Tallinn,UEFA,2 244 | Europe/Helsinki,UEFA,2 245 | Europe/Athens,UEFA,2 246 | Asia/Jerusalem,UEFA,2 247 | Europe/Vilnius,UEFA,2 248 | Europe/Riga,UEFA,2 249 | Europe/Chisinau,UEFA,2 250 | Europe/Bucharest,UEFA,2 251 | Europe/Istanbul,UEFA,2 252 | Europe/Kiev,UEFA,2 253 | Europe/Uzhgorod,UEFA,2 254 | Europe/Zaporozhye,UEFA,2 255 | Europe/Simferopol,UEFA,2 256 | Asia/Bahrain,AFC,3 257 | Asia/Baghdad,AFC,3 258 | Asia/Kuwait,AFC,3 259 | Asia/Qatar,AFC,3 260 | Asia/Riyadh,AFC,3 261 | Asia/Aden,AFC,3 262 | Indian/Mayotte,AFC,3 263 | Africa/Djibouti,CAF,3 264 | Africa/Asmara,CAF,3 265 | Africa/Addis_Ababa,CAF,3 266 | Africa/Nairobi,CAF,3 267 | Indian/Comoro,CAF,3 268 | Indian/Antananarivo,CAF,3 269 | Africa/Khartoum,CAF,3 270 | Africa/Mogadishu,CAF,3 271 | Africa/Dar_es_Salaam,CAF,3 272 | Africa/Kampala,CAF,3 273 | Europe/Kaliningrad,UEFA,3 274 | Asia/Tehran,AFC,3.5 275 | Asia/Dubai,AFC,4 276 | Asia/Muscat,AFC,4 277 | Indian/Reunion,AFC,4 278 | Indian/Mahe,AFC,4 279 | Indian/Mauritius,CAF,4 280 | Asia/Yerevan,UEFA,4 281 | Asia/Baku,UEFA,4 282 | Asia/Tbilisi,UEFA,4 283 | Europe/Moscow,UEFA,4 284 | Europe/Volgograd,UEFA,4 285 | Europe/Samara,UEFA,4 286 | Asia/Kabul,AFC,4.5 287 | Asia/Aqtobe,AFC,5 288 | Asia/Aqtau,AFC,5 289 | Asia/Oral,AFC,5 290 | Indian/Maldives,AFC,5 291 | Indian/Kerguelen,AFC,5 292 | Asia/Dushanbe,AFC,5 293 | Asia/Ashgabat,AFC,5 294 | Asia/Samarkand,AFC,5 295 | Asia/Tashkent,AFC,5 296 | Asia/Kolkata,AFC,5.5 297 | Asia/Colombo,AFC,5.5 298 | Asia/Kathmandu,AFC,5.75 299 | Asia/Dhaka,AFC,6 300 | Asia/Thimphu,AFC,6 301 | Indian/Chagos,AFC,6 302 | Asia/Bishkek,AFC,6 303 | Asia/Almaty,AFC,6 304 | Asia/Qyzylorda,AFC,6 305 | Asia/Karachi,AFC,6 306 | Asia/Yekaterinburg,UEFA,6 307 | Indian/Cocos,AFC,6.5 308 | Asia/Rangoon,AFC,6.5 309 | Indian/Christmas,AFC,7 310 | Asia/Jakarta,AFC,7 311 | Asia/Pontianak,AFC,7 312 | Asia/Phnom_Penh,AFC,7 313 | Asia/Vientiane,AFC,7 314 | Asia/Hovd,AFC,7 315 | Asia/Bangkok,AFC,7 316 | Asia/Ho_Chi_Minh,AFC,7 317 | Asia/Omsk,UEFA,7 318 | Asia/Novosibirsk,UEFA,7 319 | Asia/Novokuznetsk,UEFA,7 320 | Australia/Perth,AFC,8 321 | Asia/Brunei,AFC,8 322 | Asia/Shanghai,AFC,8 323 | Asia/Harbin,AFC,8 324 | Asia/Chongqing,AFC,8 325 | Asia/Urumqi,AFC,8 326 | Asia/Kashgar,AFC,8 327 | Asia/Hong_Kong,AFC,8 328 | Asia/Makassar,AFC,8 329 | Asia/Ulaanbaatar,AFC,8 330 | Asia/Choibalsan,AFC,8 331 | Asia/Macau,AFC,8 332 | Asia/Kuala_Lumpur,AFC,8 333 | Asia/Kuching,AFC,8 334 | Asia/Manila,AFC,8 335 | Asia/Singapore,AFC,8 336 | Asia/Taipei,AFC,8 337 | Asia/Krasnoyarsk,UEFA,8 338 | Australia/Eucla,AFC,8.75 339 | Asia/Jayapura,AFC,9 340 | Asia/Tokyo,AFC,9 341 | Asia/Pyongyang,AFC,9 342 | Asia/Seoul,AFC,9 343 | Asia/Dili,AFC,9 344 | Pacific/Palau,OFC,9 345 | Asia/Irkutsk,UEFA,9 346 | Australia/Broken_Hill,AFC,9.5 347 | Australia/Adelaide,AFC,9.5 348 | Australia/Darwin,AFC,9.5 349 | Australia/Hobart,AFC,10 350 | Australia/Currie,AFC,10 351 | Australia/Melbourne,AFC,10 352 | Australia/Sydney,AFC,10 353 | Australia/Brisbane,AFC,10 354 | Australia/Lindeman,AFC,10 355 | Pacific/Port_Moresby,AFC,10 356 | Pacific/Truk,OFC,10 357 | Pacific/Guam,OFC,10 358 | Pacific/Saipan,OFC,10 359 | Asia/Yakutsk,UEFA,10 360 | Australia/Lord_Howe,AFC,10.5 361 | Pacific/Ponape,OFC,11 362 | Pacific/Kosrae,OFC,11 363 | Pacific/Noumea,OFC,11 364 | Pacific/Guadalcanal,OFC,11 365 | Pacific/Efate,OFC,11 366 | Asia/Vladivostok,UEFA,11 367 | Asia/Sakhalin,UEFA,11 368 | Pacific/Fiji,OFC,12 369 | Pacific/Tarawa,OFC,12 370 | Pacific/Majuro,OFC,12 371 | Pacific/Kwajalein,OFC,12 372 | Pacific/Nauru,OFC,12 373 | Pacific/Auckland,OFC,12 374 | Pacific/Funafuti,OFC,12 375 | Pacific/Wake,OFC,12 376 | Pacific/Wallis,OFC,12 377 | Asia/Magadan,UEFA,12 378 | Asia/Kamchatka,UEFA,12 379 | Asia/Anadyr,UEFA,12 380 | Pacific/Chatham,OFC,12.75 381 | Pacific/Enderbury,OFC,13 382 | Pacific/Tongatapu,OFC,13 383 | Pacific/Kiritimati,OFC,14 384 | -------------------------------------------------------------------------------- /tests/test_overview.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from datetime import date 3 | 4 | import pytest 5 | from sqlalchemy.sql import func 6 | from sqlalchemy.exc import DataError, IntegrityError 7 | 8 | import marcotti.models.common.enums as enums 9 | import marcotti.models.common.overview as mco 10 | 11 | 12 | def test_country_insert(session): 13 | """Country 001: Insert a single record into Countries table and verify data.""" 14 | england = mco.Countries(name=u'England', code="ENG", confederation=enums.ConfederationType.europe) 15 | session.add(england) 16 | 17 | country = session.query(mco.Countries).all() 18 | 19 | assert country[0].name == u'England' 20 | assert country[0].code == "ENG" 21 | assert country[0].confederation.value == 'UEFA' 22 | assert repr(country[0]) == "".format(country[0].id) 23 | 24 | 25 | def test_country_unicode_insert(session): 26 | """Country 002: Insert a single record with Unicode characters into Countries table and verify data.""" 27 | ivory_coast = mco.Countries(name=u"Côte d'Ivoire", confederation=enums.ConfederationType.africa) 28 | session.add(ivory_coast) 29 | 30 | country = session.query(mco.Countries).filter_by(confederation=enums.ConfederationType.africa).one() 31 | 32 | assert country.name == u"Côte d'Ivoire" 33 | assert country.confederation.value == 'CAF' 34 | 35 | 36 | def test_country_name_overflow_error(session): 37 | """Country 003: Verify error if country name exceeds field length.""" 38 | too_long_name = "blahblah" * 8 39 | too_long_country = mco.Countries(name=unicode(too_long_name), confederation=enums.ConfederationType.north_america) 40 | with pytest.raises(DataError): 41 | session.add(too_long_country) 42 | session.commit() 43 | 44 | 45 | def test_country_code_error(session): 46 | too_long_code = "BOGUS" 47 | country = mco.Countries(name=unicode("Fredonia"), code=too_long_code, 48 | confederation=enums.ConfederationType.south_america) 49 | with pytest.raises(DataError): 50 | session.add(country) 51 | session.commit() 52 | 53 | 54 | def test_competition_insert(session): 55 | """Competition 001: Insert a single record into Competitions table and verify data.""" 56 | record = mco.Competitions(name=u"English Premier League", level=1) 57 | session.add(record) 58 | 59 | competition = session.query(mco.Competitions).filter_by(level=1).one() 60 | 61 | assert competition.name == u"English Premier League" 62 | assert competition.level == 1 63 | 64 | 65 | def test_competition_unicode_insert(session): 66 | """Competition 002: Insert a single record with Unicode characters into Competitions table and verify data.""" 67 | record = mco.Competitions(name=u"Süper Lig", level=1) 68 | session.add(record) 69 | 70 | competition = session.query(mco.Competitions).one() 71 | 72 | assert competition.name == u"Süper Lig" 73 | 74 | 75 | def test_competition_name_overflow_error(session): 76 | """Competition 003: Verify error if competition name exceeds field length.""" 77 | too_long_name = "leaguename" * 9 78 | record = mco.Competitions(name=unicode(too_long_name), level=2) 79 | with pytest.raises(DataError): 80 | session.add(record) 81 | session.commit() 82 | 83 | 84 | def test_domestic_competition_insert(session): 85 | """Domestic Competition 001: Insert domestic competition record and verify data.""" 86 | comp_name = u"English Premier League" 87 | comp_country = u"England" 88 | comp_level = 1 89 | record = mco.DomesticCompetitions(name=comp_name, level=comp_level, country=mco.Countries( 90 | name=comp_country, confederation=enums.ConfederationType.europe)) 91 | session.add(record) 92 | 93 | competition = session.query(mco.DomesticCompetitions).one() 94 | 95 | assert repr(competition) == "".format( 96 | comp_name, comp_country, comp_level) 97 | assert competition.name == comp_name 98 | assert competition.level == comp_level 99 | assert competition.country.name == comp_country 100 | 101 | 102 | def test_international_competition_insert(session): 103 | """International Competition 001: Insert international competition record and verify data.""" 104 | comp_name = u"UEFA Champions League" 105 | comp_confed = enums.ConfederationType.europe 106 | record = mco.InternationalCompetitions(name=comp_name, level=1, confederation=comp_confed) 107 | session.add(record) 108 | 109 | competition = session.query(mco.InternationalCompetitions).one() 110 | 111 | assert repr(competition) == "".format( 112 | comp_name, comp_confed.value 113 | ) 114 | assert competition.level == 1 115 | 116 | 117 | def test_year_insert(session): 118 | """Year 001: Insert multiple years into Years table and verify data.""" 119 | years_list = range(1990, 1994) 120 | for yr in years_list: 121 | record = mco.Years(yr=yr) 122 | session.add(record) 123 | 124 | years = session.query(mco.Years.yr).all() 125 | years_from_db = [x[0] for x in years] 126 | 127 | assert set(years_from_db) & set(years_list) == set(years_list) 128 | 129 | 130 | def test_year_duplicate_error(session): 131 | """Year 002: Verify error if year is inserted twice in Years table.""" 132 | for yr in range(1992, 1995): 133 | record = mco.Years(yr=yr) 134 | session.add(record) 135 | 136 | duplicate = mco.Years(yr=1994) 137 | with pytest.raises(IntegrityError): 138 | session.add(duplicate) 139 | session.commit() 140 | 141 | 142 | def test_season_insert(session): 143 | """Season 001: Insert records into Seasons table and verify data.""" 144 | yr_1994 = mco.Years(yr=1994) 145 | yr_1995 = mco.Years(yr=1995) 146 | 147 | season_94 = mco.Seasons(start_year=yr_1994, end_year=yr_1994) 148 | season_9495 = mco.Seasons(start_year=yr_1994, end_year=yr_1995) 149 | session.add(season_94) 150 | session.add(season_9495) 151 | 152 | seasons_from_db = [repr(obj) for obj in session.query(mco.Seasons).all()] 153 | seasons_test = ["", ""] 154 | 155 | assert set(seasons_from_db) & set(seasons_test) == set(seasons_test) 156 | 157 | 158 | def test_season_multiyr_search(session): 159 | """Season 002: Retrieve Season record using multi-year season name.""" 160 | yr_1994 = mco.Years(yr=1994) 161 | yr_1995 = mco.Years(yr=1995) 162 | season_9495 = mco.Seasons(start_year=yr_1994, end_year=yr_1995) 163 | session.add(season_9495) 164 | 165 | record = session.query(mco.Seasons).filter(mco.Seasons.name == '1994-1995').one() 166 | assert repr(season_9495) == repr(record) 167 | 168 | 169 | def test_season_multiyr_reference_date(session): 170 | """Season 003: Verify that reference date for season across two years is June 30.""" 171 | yr_1994 = mco.Years(yr=1994) 172 | yr_1995 = mco.Years(yr=1995) 173 | season_9495 = mco.Seasons(start_year=yr_1994, end_year=yr_1995) 174 | session.add(season_9495) 175 | 176 | record = session.query(mco.Seasons).filter(mco.Seasons.start_year == yr_1994).one() 177 | assert record.reference_date == date(1995, 6, 30) 178 | 179 | 180 | def test_season_singleyr_search(session): 181 | """Season 002: Retrieve Season record using multi-year season name.""" 182 | yr_1994 = mco.Years(yr=1994) 183 | season_94 = mco.Seasons(start_year=yr_1994, end_year=yr_1994) 184 | session.add(season_94) 185 | 186 | record = session.query(mco.Seasons).filter(mco.Seasons.name == '1994').one() 187 | assert repr(season_94) == repr(record) 188 | 189 | 190 | def test_season_singleyr_reference_date(session): 191 | """Season 005: Verify that reference date for season over one year is December 31.""" 192 | yr_1994 = mco.Years(yr=1994) 193 | season_94 = mco.Seasons(start_year=yr_1994, end_year=yr_1994) 194 | session.add(season_94) 195 | 196 | record = session.query(mco.Seasons).filter(mco.Seasons.start_year == yr_1994).one() 197 | assert record.reference_date == date(1994, 12, 31) 198 | 199 | 200 | def test_timezone_insert(session): 201 | """Timezone 001: Insert timezone records into Timezones table and verify data.""" 202 | timezones = [ 203 | mco.Timezones(name=u"Europe/Paris", offset=1, confederation=enums.ConfederationType.europe), 204 | mco.Timezones(name=u"America/New_York", offset=-5.0, confederation=enums.ConfederationType.north_america), 205 | mco.Timezones(name=u"Asia/Kathmandu", offset=+5.75, confederation=enums.ConfederationType.asia) 206 | ] 207 | session.add_all(timezones) 208 | 209 | tz_uefa = session.query(mco.Timezones).filter_by(confederation=enums.ConfederationType.europe).one() 210 | assert repr(tz_uefa) == "" 211 | 212 | stmt = session.query(func.min(mco.Timezones.offset).label('far_west')).subquery() 213 | tz_farwest = session.query(mco.Timezones).filter(mco.Timezones.offset == stmt.c.far_west).one() 214 | assert repr(tz_farwest) == "" 215 | 216 | stmt = session.query(func.max(mco.Timezones.offset).label('far_east')).subquery() 217 | tz_fareast = session.query(mco.Timezones).filter(mco.Timezones.offset == stmt.c.far_east).one() 218 | assert repr(tz_fareast) == "" 219 | 220 | 221 | def test_venue_generic_insert(session, venue_data): 222 | """Venue 001: Insert generic venue records into Venues table and verify data.""" 223 | session.add(mco.Venues(**venue_data)) 224 | 225 | emirates = session.query(mco.Venues).one() 226 | 227 | assert repr(emirates) == u"" 228 | assert emirates.region is None 229 | assert emirates.latitude == 51.555000 230 | assert emirates.longitude == -0.108611 231 | assert emirates.altitude == 41 232 | assert repr(emirates.timezone) == "" 233 | 234 | 235 | def test_venue_empty_coordinates(session, venue_data): 236 | """Venue 002: Verify that lat/long/alt coordinates are zeroed if not present in Venues object definition.""" 237 | revised_venue_data = {key: value for key, value in venue_data.items() 238 | if key not in ['latitude', 'longitude', 'altitude']} 239 | session.add(mco.Venues(**revised_venue_data)) 240 | 241 | emirates = session.query(mco.Venues).one() 242 | 243 | assert emirates.latitude == 0.000000 244 | assert emirates.longitude == 0.000000 245 | assert emirates.altitude == 0 246 | 247 | 248 | def test_venue_latitude_error(session, venue_data): 249 | """Venue 003: Verify error if latitude of match venue exceeds range.""" 250 | for direction in [-1, 1]: 251 | venue_data['latitude'] = 92.123456 * direction 252 | venue = mco.Venues(**venue_data) 253 | with pytest.raises(IntegrityError): 254 | session.add(venue) 255 | session.commit() 256 | session.rollback() 257 | 258 | 259 | def test_venue_longitude_error(session, venue_data): 260 | """Venue 004: Verify error if longitude of match venue exceeds range.""" 261 | for direction in [-1, 1]: 262 | venue_data['longitude'] = 200.000000 * direction 263 | venue = mco.Venues(**venue_data) 264 | with pytest.raises(IntegrityError): 265 | session.add(venue) 266 | session.commit() 267 | session.rollback() 268 | 269 | 270 | def test_venue_altitude_error(session, venue_data): 271 | """Venue 005: Verify error if altitude of match venue is out of range.""" 272 | for out_of_range in [-205, 4600]: 273 | venue_data['altitude'] = out_of_range 274 | venue = mco.Venues(**venue_data) 275 | with pytest.raises(IntegrityError): 276 | session.add(venue) 277 | session.commit() 278 | session.rollback() 279 | 280 | 281 | def test_venue_history_insert(session, venue_data, venue_config): 282 | """Venue 006: Insert venue history data into VenueHistory model and verify data.""" 283 | emirates = mco.Venues(**venue_data) 284 | venue_config['venue'] = emirates 285 | emirates_config = mco.VenueHistory(**venue_config) 286 | session.add(emirates_config) 287 | 288 | record = session.query(mco.VenueHistory).one() 289 | 290 | assert repr(record) == u"" 292 | assert record.seats == 60361 293 | assert record.surface.description == u"Desso GrassMaster" 294 | assert record.surface.type == enums.SurfaceType.hybrid 295 | 296 | 297 | def test_venue_history_empty_numbers(session, venue_data, venue_config): 298 | """Venue 007: Verify that length/width/capacity/seats fields are set to default if missing in VenueHistory data.""" 299 | emirates = mco.Venues(**venue_data) 300 | venue_config['venue'] = emirates 301 | revised_venue_config = {key: value for key, value in venue_config.items() 302 | if key not in ['length', 'width', 'capacity', 'seats']} 303 | emirates_config = mco.VenueHistory(**revised_venue_config) 304 | session.add(emirates_config) 305 | 306 | record = session.query(mco.VenueHistory).one() 307 | 308 | assert record.length == 105 309 | assert record.width == 68 310 | assert record.capacity == 0 311 | assert record.seats == 0 312 | 313 | 314 | def test_venue_history_field_dimension_error(session, venue_data, venue_config): 315 | """Venue 007: Verify error if length/width fields in VenueHistory data are out of range.""" 316 | emirates = mco.Venues(**venue_data) 317 | venue_config['venue'] = emirates 318 | for field, values in zip(['length', 'width'], [(85, 125), (40, 95)]): 319 | for out_of_range in values: 320 | venue_config[field] = out_of_range 321 | emirates_config = mco.VenueHistory(**venue_config) 322 | with pytest.raises(IntegrityError): 323 | session.add(emirates_config) 324 | session.commit() 325 | session.rollback() 326 | 327 | 328 | def test_venue_history_capacity_error(session, venue_data, venue_config): 329 | """Venue 007: Verify error if length/width fields in VenueHistory data are out of range.""" 330 | emirates = mco.Venues(**venue_data) 331 | venue_config['venue'] = emirates 332 | for field in ['capacity', 'seats']: 333 | new_venue_config = dict(venue_config, **{field: -1}) 334 | emirates_config = mco.VenueHistory(**new_venue_config) 335 | with pytest.raises(IntegrityError): 336 | session.add(emirates_config) 337 | session.commit() 338 | session.rollback() 339 | 340 | 341 | def test_surface_generic_insert(session): 342 | """Playing Surface 001: Insert playing surface data into Surfaces model and verify data.""" 343 | surfaces = [ 344 | mco.Surfaces(description=u"Perennial ryegrass", type=enums.SurfaceType.natural), 345 | mco.Surfaces(description=u"Desso GrassMaster", type=enums.SurfaceType.hybrid), 346 | mco.Surfaces(description=u"FieldTurf", type=enums.SurfaceType.artificial) 347 | ] 348 | session.add_all(surfaces) 349 | 350 | natural = session.query(mco.Surfaces).filter_by(type=enums.SurfaceType.natural).one() 351 | 352 | assert repr(natural) == u"" 353 | 354 | 355 | def test_surface_empty_description_error(session): 356 | """Playing Surface 002: Verify error if description field for Surfaces model is empty.""" 357 | surface = mco.Surfaces(type=enums.SurfaceType.natural) 358 | with pytest.raises(IntegrityError): 359 | session.add(surface) 360 | session.commit() 361 | -------------------------------------------------------------------------------- /marcotti/etl/base/transform.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | import marcotti.models.club as mc 4 | import marcotti.models.common.enums as enums 5 | import marcotti.models.common.overview as mco 6 | import marcotti.models.common.personnel as mcp 7 | import marcotti.models.common.suppliers as mcs 8 | from .workflows import WorkflowBase 9 | 10 | 11 | class MarcottiTransform(WorkflowBase): 12 | """ 13 | Transform and validate extracted data. 14 | """ 15 | 16 | @staticmethod 17 | def suppliers(data_frame): 18 | return data_frame 19 | 20 | @staticmethod 21 | def years(data_frame): 22 | return data_frame 23 | 24 | @staticmethod 25 | def seasons(data_frame): 26 | return data_frame 27 | 28 | def competitions(self, data_frame): 29 | if 'country' in data_frame.columns: 30 | transformed_field = 'country' 31 | lambdafunc = lambda x: pd.Series(self.get_id(mco.Countries, name=x[transformed_field])) 32 | id_frame = data_frame.apply(lambdafunc, axis=1) 33 | id_frame.columns = ['country_id'] 34 | elif 'confed' in data_frame.columns: 35 | transformed_field = 'confed' 36 | lambdafunc = lambda x: pd.Series(enums.ConfederationType.from_string(x[transformed_field])) 37 | id_frame = data_frame.apply(lambdafunc, axis=1) 38 | id_frame.columns = ['confederation'] 39 | else: 40 | raise KeyError("Cannot insert Competition record: No Country or Confederation data present") 41 | return data_frame.join(id_frame).drop(transformed_field, axis=1) 42 | 43 | def countries(self, data_frame): 44 | lambdafunc = lambda x: pd.Series(enums.ConfederationType.from_string(x['confed'])) 45 | id_frame = data_frame.apply(lambdafunc, axis=1) 46 | id_frame.columns = ['confederation'] 47 | joined_frame = data_frame.join(id_frame).drop('confed', axis=1) 48 | return joined_frame 49 | 50 | def clubs(self, data_frame): 51 | if 'country' in data_frame.columns: 52 | lambdafunc = lambda x: pd.Series(self.get_id(mco.Countries, name=x['country'])) 53 | id_frame = data_frame.apply(lambdafunc, axis=1) 54 | id_frame.columns = ['country_id'] 55 | else: 56 | raise KeyError("Cannot insert Club record: No Country data present") 57 | return data_frame.join(id_frame) 58 | 59 | def venues(self, data_frame): 60 | lambdafunc = lambda x: pd.Series([ 61 | self.get_id(mco.Countries, name=x['country']), 62 | self.get_id(mco.Timezones, name=x['timezone']), 63 | self.get_id(mco.Surfaces, description=x['surface']), 64 | self.make_date_object(x['config_date']) 65 | ]) 66 | ids_frame = data_frame.apply(lambdafunc, axis=1) 67 | ids_frame.columns = ['country_id', 'timezone_id', 'surface_id', 'eff_date'] 68 | joined_frame = data_frame.join(ids_frame).drop(['country', 'timezone', 'surface', 'config_date'], axis=1) 69 | new_frame = joined_frame.where((pd.notnull(joined_frame)), None) 70 | return new_frame 71 | 72 | def timezones(self, data_frame): 73 | lambdafunc = lambda x: pd.Series(enums.ConfederationType.from_string(x['confed'])) 74 | id_frame = data_frame.apply(lambdafunc, axis=1) 75 | id_frame.columns = ['confederation'] 76 | joined_frame = data_frame.join(id_frame).drop('confed', axis=1) 77 | return joined_frame 78 | 79 | def positions(self, data_frame): 80 | lambdafunc = lambda x: pd.Series(enums.PositionType.from_string(x['position_type'])) 81 | id_frame = data_frame.apply(lambdafunc, axis=1) 82 | id_frame.columns = ['type'] 83 | joined_frame = data_frame.join(id_frame).drop('position_type', axis=1) 84 | return joined_frame 85 | 86 | def surfaces(self, data_frame): 87 | lambdafunc = lambda x: pd.Series(enums.SurfaceType.from_string(x['surface_type'])) 88 | id_frame = data_frame.apply(lambdafunc, axis=1) 89 | id_frame.columns = ['type'] 90 | joined_frame = data_frame.join(id_frame).drop('surface_type', axis=1) 91 | return joined_frame 92 | 93 | def players(self, data_frame): 94 | lambdafunc = lambda x: pd.Series([ 95 | self.make_date_object(x['dob']), 96 | enums.NameOrderType.from_string(x['name_order'] or 'Western'), 97 | self.get_id(mco.Countries, name=x['country']), 98 | self.get_id(mcs.PositionMap, remote_id=x['remote_position_id'], supplier_id=self.supplier_id) 99 | ]) 100 | ids_frame = data_frame.apply(lambdafunc, axis=1) 101 | ids_frame.columns = ['birth_date', 'order', 'country_id', 'position_id'] 102 | joined_frame = data_frame.join(ids_frame).drop( 103 | ['dob', 'name_order', 'country', 'remote_position_id'], axis=1) 104 | return joined_frame 105 | 106 | def managers(self, data_frame): 107 | lambdafunc = lambda x: pd.Series([ 108 | self.make_date_object(x['dob']), 109 | enums.NameOrderType.from_string(x['name_order'] or 'Western'), 110 | self.get_id(mco.Countries, name=x['country']) 111 | ]) 112 | ids_frame = data_frame.apply(lambdafunc, axis=1) 113 | ids_frame.columns = ['birth_date', 'order', 'country_id'] 114 | joined_frame = data_frame.join(ids_frame).drop(['dob', 'name_order', 'country'], axis=1) 115 | return joined_frame 116 | 117 | def referees(self, data_frame): 118 | lambdafunc = lambda x: pd.Series([ 119 | self.make_date_object(x['dob']), 120 | enums.NameOrderType.from_string(x['name_order'] or 'Western'), 121 | self.get_id(mco.Countries, name=x['country']) 122 | ]) 123 | ids_frame = data_frame.apply(lambdafunc, axis=1) 124 | ids_frame.columns = ['birth_date', 'order', 'country_id'] 125 | joined_frame = data_frame.join(ids_frame).drop(['dob', 'name_order', 'country'], axis=1) 126 | return joined_frame 127 | 128 | def league_matches(self, data_frame): 129 | lambdafunc = lambda x: pd.Series([ 130 | self.get_id(mco.Competitions, name=x['competition']), 131 | self.get_id(mco.Seasons, name=x['season']), 132 | self.get_id(mco.Venues, name=x['venue']), 133 | self.get_id(mc.Clubs, name=x['home_team']), 134 | self.get_id(mc.Clubs, name=x['away_team']), 135 | self.get_id(mcp.Managers, full_name=x['home_manager']), 136 | self.get_id(mcp.Managers, full_name=x['away_manager']), 137 | self.get_id(mcp.Referees, full_name=x['referee']), 138 | self.make_date_object(x['date']), 139 | enums.WeatherConditionType.from_string(x['kickoff_wx']) if x['kickoff_wx'] else None, 140 | enums.WeatherConditionType.from_string(x['halftime_wx']) if x['halftime_wx'] else None, 141 | enums.WeatherConditionType.from_string(x['fulltime_wx']) if x['fulltime_wx'] else None 142 | ]) 143 | ids_frame = data_frame.apply(lambdafunc, axis=1) 144 | ids_frame.columns = ['competition_id', 'season_id', 'venue_id', 'home_team_id', 'away_team_id', 145 | 'home_manager_id', 'away_manager_id', 'referee_id', 'match_date', 146 | 'kickoff_weather', 'halftime_weather', 'fulltime_weather'] 147 | columns_to_drop = ['competition', 'season', 'venue', 'home_team', 'away_team', 'home_manager', 148 | 'away_manager', 'referee', 'date', 'kickoff_wx', 'halftime_wx', 'fulltime_wx'] 149 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 150 | 151 | def knockout_matches(self, data_frame): 152 | lambdafunc = lambda x: pd.Series([ 153 | self.get_id(mco.Competitions, name=x['competition']), 154 | self.get_id(mco.Seasons, name=x['season']), 155 | self.get_id(mco.Venues, name=x['venue']), 156 | self.get_id(mc.Clubs, name=x['home_team']), 157 | self.get_id(mc.Clubs, name=x['away_team']), 158 | self.get_id(mcp.Managers, full_name=x['home_manager']), 159 | self.get_id(mcp.Managers, full_name=x['away_manager']), 160 | self.get_id(mcp.Referees, full_name=x['referee']), 161 | enums.KnockoutRoundType.from_string(x['round']), 162 | self.make_date_object(x['date']), 163 | enums.WeatherConditionType.from_string(x['kickoff_wx']) if x['kickoff_wx'] else None, 164 | enums.WeatherConditionType.from_string(x['halftime_wx']) if x['halftime_wx'] else None, 165 | enums.WeatherConditionType.from_string(x['fulltime_wx']) if x['fulltime_wx'] else None 166 | ]) 167 | ids_frame = data_frame.apply(lambdafunc, axis=1) 168 | ids_frame.columns = ['competition_id', 'season_id', 'venue_id', 'home_team_id', 'away_team_id', 169 | 'home_manager_id', 'away_manager_id', 'referee_id', 'ko_round', 'match_date', 170 | 'kickoff_weather', 'halftime_weather', 'fulltime_weather'] 171 | columns_to_drop = ['competition', 'season', 'venue', 'home_team', 'away_team', 'home_manager', 172 | 'away_manager', 'referee', 'date', 'round', 'kickoff_wx', 'halftime_wx', 'fulltime_wx'] 173 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 174 | 175 | def group_matches(self, data_frame): 176 | lambdafunc = lambda x: pd.Series([ 177 | self.get_id(mco.Competitions, name=x['competition']), 178 | self.get_id(mco.Seasons, name=x['season']), 179 | self.get_id(mco.Venues, name=x['venue']), 180 | self.get_id(mc.Clubs, name=x['home_team']), 181 | self.get_id(mc.Clubs, name=x['away_team']), 182 | self.get_id(mcp.Managers, full_name=x['home_manager']), 183 | self.get_id(mcp.Managers, full_name=x['away_manager']), 184 | self.get_id(mcp.Referees, full_name=x['referee']), 185 | enums.GroupRoundType.from_string(x['round']), 186 | self.make_date_object(x['date']), 187 | enums.WeatherConditionType.from_string(x['kickoff_wx']) if x['kickoff_wx'] else None, 188 | enums.WeatherConditionType.from_string(x['halftime_wx']) if x['halftime_wx'] else None, 189 | enums.WeatherConditionType.from_string(x['fulltime_wx']) if x['fulltime_wx'] else None 190 | ]) 191 | ids_frame = data_frame.apply(lambdafunc, axis=1) 192 | ids_frame.columns = ['competition_id', 'season_id', 'venue_id', 'home_team_id', 'away_team_id', 193 | 'home_manager_id', 'away_manager_id', 'referee_id', 'group_round', 'match_date', 194 | 'kickoff_weather', 'halftime_weather', 'fulltime_weather'] 195 | columns_to_drop = ['competition', 'season', 'venue', 'home_team', 'away_team', 'home_manager', 196 | 'away_manager', 'referee', 'date', 'round', 'kickoff_wx', 'halftime_wx', 'fulltime_wx'] 197 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 198 | 199 | def match_lineups(self, data_frame): 200 | lambdafunc = lambda x: pd.Series([ 201 | self.get_id(mc.ClubLeagueMatches, 202 | competition_id=self.get_id(mco.Competitions, name=x['competition']), 203 | season_id=self.get_id(mco.Seasons, name=x['season']), 204 | matchday=x['matchday'], 205 | home_team_id=self.get_id(mc.Clubs, name=x['home_team']), 206 | away_team_id=self.get_id(mc.Clubs, name=x['away_team'])), 207 | self.get_id(mc.Clubs, name=x['player_team']), 208 | self.get_id(mcp.Players, full_name=x['player_name']) 209 | ]) 210 | ids_frame = data_frame.apply(lambdafunc, axis=1) 211 | ids_frame.columns = ['match_id', 'team_id', 'player_id'] 212 | columns_to_drop = ['competition', 'season', 'matchday', 'home_team', 'away_team'] 213 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 214 | 215 | def goals(self, data_frame): 216 | lambdafunc = lambda x: pd.Series([ 217 | self.get_id(mc.ClubMatchLineups, 218 | match_id=self.get_id(mcs.MatchMap, remote_id=x['remote_match_id'], 219 | supplier_id=self.supplier_id), 220 | player_id=self.get_id(mcp.Players, full_name=x['scorer'])), 221 | self.get_id(mc.Clubs, name=x['scoring_team']), 222 | enums.ShotEventType.from_string(x['scoring_event']), 223 | enums.BodypartType.from_string(x['bodypart_desc']) 224 | ]) 225 | ids_frame = data_frame.apply(lambdafunc, axis=1) 226 | ids_frame.columns = ['lineup_id', 'team_id', 'event', 'bodypart'] 227 | columns_to_drop = ['remote_match_id', 'scorer', 'scoring_team', 'scoring_event', 'bodypart_desc'] 228 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 229 | 230 | def penalties(self, data_frame): 231 | lambdafunc = lambda x: pd.Series([ 232 | self.get_id(mc.ClubMatchLineups, 233 | match_id=self.get_id(mcs.MatchMap, remote_id=x['remote_match_id'], 234 | supplier_id=self.supplier_id), 235 | player_id=self.get_id(mcp.Players, full_name=x['penalty_taker'])), 236 | enums.FoulEventType.from_string(x['penalty_foul']), 237 | enums.ShotOutcomeType.from_string(x['penalty_outcome']) 238 | ]) 239 | ids_frame = data_frame.apply(lambdafunc, axis=1) 240 | ids_frame.columns = ['lineup_id', 'foul', 'outcome'] 241 | columns_to_drop = ['remote_match_id', 'penalty_taker', 'penalty_foul', 'penalty_outcome'] 242 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 243 | 244 | def bookables(self, data_frame): 245 | lambdafunc = lambda x: pd.Series([ 246 | self.get_id(mc.ClubMatchLineups, 247 | match_id=self.get_id(mcs.MatchMap, remote_id=x['remote_match_id'], 248 | supplier_id=self.supplier_id), 249 | player_id=self.get_id(mcp.Players, full_name=x['player'])), 250 | enums.FoulEventType.from_string(x['foul_desc']), 251 | enums.CardType.from_string(x['card_type']) 252 | ]) 253 | ids_frame = data_frame.apply(lambdafunc, axis=1) 254 | ids_frame.columns = ['lineup_id', 'foul', 'card'] 255 | columns_to_drop = ['remote_match_id', 'player', 'foul_desc', 'card_type'] 256 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 257 | 258 | def substitutions(self, data_frame): 259 | lambdafunc = lambda x: pd.Series([ 260 | self.get_id(mc.ClubMatchLineups, 261 | match_id=self.get_id(mcs.MatchMap, remote_id=x['remote_match_id'], 262 | supplier_id=self.supplier_id), 263 | player_id=self.get_id(mcp.Players, full_name=x['in_player_name'])), 264 | self.get_id(mc.ClubMatchLineups, 265 | match_id=self.get_id(mcs.MatchMap, remote_id=x['remote_match_id'], 266 | supplier_id=self.supplier_id), 267 | player_id=self.get_id(mcp.Players, full_name=x['out_player_name'])) 268 | ]) 269 | ids_frame = data_frame.apply(lambdafunc, axis=1) 270 | ids_frame.columns = ['lineup_in_id', 'lineup_out_id'] 271 | columns_to_drop = ['remote_match_id', 'in_player_name', 'out_player_name'] 272 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 273 | 274 | def penalty_shootouts(self, data_frame): 275 | lambdafunc = lambda x: pd.Series([ 276 | self.get_id(mc.ClubMatchLineups, 277 | match_id=self.get_id(mcs.MatchMap, remote_id=x['remote_match_id'], supplier_id=self.supplier_id), 278 | player_id=self.get_id(mcp.Players, full_name=x['penalty_taker'])), 279 | enums.ShotOutcomeType.from_string(x['penalty_outcome']) 280 | ]) 281 | ids_frame = data_frame.apply(lambdafunc, axis=1) 282 | ids_frame.columns = ['lineup_id', 'outcome'] 283 | columns_to_drop = ['remote_match_id', 'penalty_taker', 'penalty_outcome'] 284 | return data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 285 | 286 | 287 | class MarcottiStatsTransform(MarcottiTransform): 288 | 289 | categories = ['assists', 'clearances', 'corner_crosses', 'corners', 'crosses', 'defensives', 290 | 'discipline', 'duels', 'foul_wins', 'freekicks', 'gk_actions', 'gk_allowed_goals', 291 | 'gk_allowed_shots', 'gk_saves', 'goal_bodyparts', 'goal_locations', 'goal_totals', 292 | 'goalline_clearances', 'important_plays', 'pass_directions', 'pass_lengths', 293 | 'pass_locations', 'pass_totals', 'penalty_actions', 'shot_blocks', 'shot_bodyparts', 294 | 'shot_locations', 'shot_plays', 'shot_totals', 'tackles', 'throwins', 'touch_locations', 295 | 'touches'] 296 | 297 | def __init__(self, session, supplier): 298 | super(MarcottiStatsTransform, self).__init__(session, supplier) 299 | for category in MarcottiStatsTransform.categories: 300 | add_stats_fn(category) 301 | 302 | 303 | def add_stats_fn(category): 304 | def fn(self, data_frame): 305 | lambdafunc = lambda x: pd.Series([ 306 | self.get_id(mcs.PlayerMap, remote_id=x['remote_player_id'], supplier_id=self.supplier_id), 307 | self.get_id(mc.ClubMap, remote_id=x['remote_player_team_id'], supplier_id=self.supplier_id), 308 | self.get_id(mc.ClubMap, remote_id=x['remote_opposing_team_id'], supplier_id=self.supplier_id) 309 | ]) 310 | ids_frame = data_frame.apply(lambdafunc, axis=1) 311 | ids_frame.columns = ['player_id', 'player_team_id', 'opposing_team_id'] 312 | columns_to_drop = ['remote_player_id', 'remote_player_team_id', 'remote_opposing_team_id'] 313 | inter_frame = data_frame.join(ids_frame).drop(columns_to_drop, axis=1) 314 | outerlambdafunc = lambda x: pd.Series([ 315 | self.get_id(mc.ClubMatchLineups, 316 | match_id=self.get_id( 317 | mc.ClubLeagueMatches, 318 | home_team_id=x['player_team_id'] if x['locale'] == 'Home' else x['opposing_team_id'], 319 | away_team_id=x['opposing_team_id'] if x['locale'] == 'Away' else x['player_team_id'], 320 | date=x['match_date']), 321 | player_id=x['player_id']) 322 | ]) 323 | outerids_frame = inter_frame.apply(outerlambdafunc, axis=1) 324 | outerids_frame.columns = ['lineup_id'] 325 | more_columns_to_drop = ['player_team_id', 'opposing_team_id', 'match_date', 'locale', 'player_id'] 326 | return inter_frame.join(outerids_frame).drop(more_columns_to_drop, axis=1) 327 | 328 | setattr(MarcottiStatsTransform, category, fn) 329 | 330 | fn.__name__ = category 331 | fn.__doc__ = "Data transformation for {} method".format(category) 332 | --------------------------------------------------------------------------------