# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['etl_manager']

package_data = \
{'': ['*'], 'etl_manager': ['specs/*']}

install_requires = \
['boto3>=1.9.205',
 'jsonschema>=3.0.0',
 'parameterized>=0.7.0,<0.8.0',
 'regex>=2020.7.14,<2021.0.0']

setup_kwargs = {
    'name': 'etl-manager',
    'version': '7.5.0',
    'description': 'A python package to manage etl processes on AWS',
    'long_description': '# etl_manager\n\n[![Actions Status](https://github.com/moj-analytical-services/etl_manager/workflows/ETL%20Manager/badge.svg)](https://github.com/moj-analytical-services/etl_manager/actions)\n\nA python package that manages our data engineering framework and implements them on AWS Glue.\n\nThe main functionality of this package is to interact with AWS Glue to create meta data catalogues and run Glue jobs.\n\nTo install:\n\n```bash\npip install etl_manager\n```\n\n## Meta Data\n\nLet\'s say I have a single table (a csv file) and I want to query it using Amazon athena. My csv file is in the following S3 path: `s3://my-bucket/my-table-folder/file.csv`.\n\nfile.csv is a table that looks like this:\n\n| col1 | col2 |\n|------|------|\n| a    | 1    |\n| b    | 12   |\n| c    | 42   |\n\nAs you can see col1 is a string and col2 is a integer.\n\n> **Notes:**\n> - for Athena to work your table should not contain a header. So before file.csv is uploaded to S3 you should make sure it has no header.\n> - Tables must be in a folder. I.e. the location of your table (`table.location`) should be the parent folder of where you data exists. See example below.\n\nTo create a schema for your data to be queried by Athena you can use the following code:\n\n```python\nfrom etl_manager.meta import DatabaseMeta, TableMeta\n\n# Create database meta object\ndb = DatabaseMeta(name = \'my_database\', bucket=\'my-bucket\')\n\n# Create table meta object\ntab = TableMeta(name = \'my_table\', location = \'my-table-folder\')\n\n# Add column defintions to the table\ntab.add_column(name = \'col1\', \'character\', description = \'column contains a letter\')\ntab.add_column(name = \'col2\', \'int\', description = \'column contains a number\')\n\n# Add table to the database\ndb.add_table(tab)\n\n# Create the table on AWS glue\ndb.create_glue_database()\n```\n\nNow the table can be queried via SQL e.g. `SELECT * FROM my_database.my_table`\n\n### Meta data structure\n\nCurrently at very simple level. Assume you have the following folder structure from example code:\n\n```\nmeta_data/\n--- database.json\n--- teams.json\n--- employees.json\n```\n\ndatabase.json is a special json file that holds the meta data for the database. In our example it looks like this:\n\n```json\n{\n    "description": "Example database",\n    "name": "workforce",\n    "bucket": "my-bucket",\n    "base_folder": "database/database1"\n}\n```\n\nWhen you create your database it will have this `name` and `description`. The `bucket` key specifies where the database exists (and therefore where the tables exist) in S3. The `base_folder` is is the initial path to where the tables exist. If your tables are in folders directly in the bucket (e.g. `s3://my-bucket/table1/`) then you can leave base_folder as an empty string (`""`).\n\nThe employees table has an ID for each employee their name and dob. The table meta looks like this:\n\n```json\n{\n    "$schema" : "https://moj-analytical-services.github.io/metadata_schema/table/v1.0.0.json",\n    "name": "employees",\n    "description": "table containing employee information",\n    "data_format": "parquet",\n    "location": "employees/",\n    "columns": [\n        {\n            "name": "employee_id",\n            "type": "int",\n            "description": "an ID for each employee"\n        },\n        {\n            "name": "employee_name",\n            "type": "character",\n            "description": "name of the employee"\n        },\n        {\n            "name": "employee_dob",\n            "type": "date",\n            "description": "date of birth for the employee"\n        }\n    ]\n}\n```\n\n**Currently supported data types for your columns currently are:**\n\n`character | int | long | float | double | decimal | date | datetime |  boolean`\n\nThis is a standard layout for a table metadata json. `$schema` points to another json that validates the structure of out table metadata files. Your table will have this `name` and `description` _(Note: It is strongly suggested that the name of your table matches the name of the metadata json file)_ when the database is created. The `location` is the relative folder path to where your table exists. This path is relative to your database `base_folder`. This means that the full path your table is `s3://<database.bucket>/<database.base_folder>/<table.folder>/`. So in this example the table employees should be in the s3 path `s3://my-bucket/database/database1/employees`. The `data_format` specifies what type of data the table is. Finally your columns is an array of objects. Where each object is a column definition specifying the `name`, `description` and `type` (data type) of the column. Each column can have optional arguments `pattern`, `enum` and `nullable` (see [table_schema.json](https://moj-analytical-services.github.io/metadata_schema/table/v1.0.0.json) for definition).\n\n**Note:** that the order of the columns listed here should be the order of the columns in the table _(remember that data for a table should not have a header so the data will be queried wrong if the column order does not match up with the actual data)_.\n\nHere is another table in the database called teams. The teams table is a list of employee IDs for each team. Showing which employees are in each team. This table is taken each month (so you can see which employee was in which team each month). Therefore this table is partitioned by each monthly snapshot.\n\n```json\n{\n    "$schema" : "https://moj-analytical-services.github.io/metadata_schema/table/v1.0.0.json",\n    "name": "teams",\n    "description": "month snapshot of which employee with working in what team",\n    "data_format": "parquet",\n    "location": "teams/",\n    "columns": [\n        {\n            "name": "team_id",\n            "type": "int",\n            "description": "ID given to each team",\n            "nullable" : false\n        },\n        {\n            "name": "team_name",\n            "type": "character",\n            "description": "name of the team"\n        },\n        {\n            "name": "employee_id",\n            "type": "int",\n            "description": "primary key for each employee in the employees table",\n            "pattern" : "\\\\d+"\n        },\n        {\n            "name": "snapshot_year",\n            "type": "int",\n            "description": "year at which snapshot of workforce was taken"\n        },\n        {\n            "name": "snapshot_month",\n            "type": "int",\n            "description": "month at which snapshot of workforce was taken",\n            "enum" : [1,2,3,4,5,6,7,8,9,10,11,12]\n        }\n    ],\n    "partitions" : ["snapshot_year", "snapshot_month"]\n}\n```\n\nFrom the above you can see this has additional properties `enum`, `pattern`, `nullable` and a `partitions` property:\n\n- **enum:** What values the column can take (does not have to include nulls - should use nullable property)\n- **pattern:** Values in this column should match this regex string in the pattern property\n- **nullable:** Specifies if this column should accept `NULL` values.\n- **partitions:** Specifies if any of the columns in the table are file partitions rather than columns in the data. `etl_manager` will force your meta data json files to have columns that are partitions at the end of your data\'s list of columns.\n\n> **Note:** etl_manager does not enforce information provided by `enum`, `pattern` and `nullable`. It is just there to provide information to other tools or functions that could use this information to validate your data. Also the information in `pattern`, `enum` and `nullable` can conflict etl_manager does not check for conflicts. For example a column with an enum of `[0,1]` and a pattern of `[A-Za-z]` is allowed.\n\n### Examples using the DatabaseMeta Class\n\nThe easiest way to create a database is to run the code below. It reads a database schema based on the json files in a folder and creates this database meta in the glue catalogue. Allowing you to query the data using SQL using Athena.\n\n```python\nfrom etl_manager.meta import read_database_folder\ndb = read_database_folder(\'example_meta_data/\')\ndb.create_glue_database()\n```\n\nThe code snippet below creates a database meta object that allows you to manipulate the database and the tables that exist in it\n\n```python\nfrom etl_manager.meta import read_database_folder\n\ndb = read_database_folder(\'example_meta_data/\')\n\n# Database has callable objects\n\ndb.name # workforce\n\ndb.table_names # [employees, teams]\n\n# Each table in the database is an object from the TableMeta Class which can be callable from the database meta object\n\ndb.table(\'employees\').columns # returns all columns in employees table\n\n# The db and table object properties can also be altered and updated\n\ndb.name = \'new_db_name\'\ndb.name # \'new_db_name\n\ndb.table(\'employees\').name = \'new_name\'\n\ndb.table_names # [new_name, teams]\n\ndb.remove_table(\'new_name\')\n\ndb.name # workforce_dev (note as default the package adds _dev if a db_suffix is not provided in DatabaseMeta)\n\n# Set all table types to parquet and create database schema in glue\nfor t in db_table_names :\n    db.table(t).data_format = \'parquet\'\ndb.create_glue_database()\n```\n\n## Using the GlueJob Class\n\nThe GlueJob class can be used to run pyspark jobs on AWS Glue. It is worth keeping up to date with AWS release notes and general guidance on running Glue jobs. This class is a wrapper function to simplify running glue jobs by using a structured format.\n\n```python\nfrom etl_manager.etl import GlueJob\n\nmy_role = \'aws_role\'\nbucket = \'bucket-to-store-temp-glue-job-in\'\n\njob = GlueJob(\'glue_jobs/simple_etl_job/\', bucket=bucket, job_role=my_role, job_arguments={"--test_arg" : \'some_string\'})\njob.run_job()\n\nprint(job.job_status)\n```\n\n### Glue Job Folder Structure\n\nGlue jobs have the prescribed folder format as follows:\n\n```\n├── glue_jobs/\n|   |\n│   ├── job1/\n|   |   ├── job.py\n|   |   ├── glue_resources/\n|   |   |   └── my_lookup_table.csv\n|   |   └── glue_py_resources/\n|   |       ├── my_python_functions.zip\n|   |       └── github_zip_urls.txt\n|   |   └── glue_jars/\n|   |       └── my_jar.jar\n|   |\n│   ├── job2/\n│   |   ├── job.py\n│   |   ├── glue_resources/\n│   |   └── glue_py_resources/\n|   |\n|   ├── shared_job_resources/\n│   |   ├── glue_resources/\n|   |   |   └── meta_data_dictionary.json\n│   |   └── glue_py_resources/\n|   |   └── glue_jars/\n|   |       └── my_other_jar.jar\n```\n\nEvery glue job folder must have a `job.py` script in that folder. That is the only required file everything else is optional. When you want to create a glue job object you point the GlueJob class to the parent folder of the `job.py` script you want to run. There are two additional folders you can add to this parent folder :\n\n#### glue_resources folder\n\nAny files in this folder are uploaded to the working directory of the glue job. This means in your `job.py` script you can get the path to these files by:\n\n```python\npath_to_file = os.path.join(os.getcwd(), \'file_in_folder.txt\')\n```\n\nThe GlueJob class will only upload files with extensions (.csv, .sql, .json, .txt) to S3 for the glue job to access.\n\n#### glue_py_resources\n\nThese are python scripts you can import in your `job.py` script. e.g. if I had a `utils.py` script in my glue_py_resources folder. I could import that script normally e.g.\n\n```python\nfrom utils import *\n```\n\nYou can also supply zip file which is a group of python functions in the standard python package structure. You can then reference this package as you would normally in python. For example if I had a package zipped as `my_package.zip` in the glue_py_resources folder then you could access that package normally in your job script like:\n\n```python\nfrom my_package.utils import *\n```\n\nYou can also supply a text file with the special name `github_zip_urls.txt`. This is a text file where each line is a path to a github zip ball. The GlueJob class will download the github package rezip it and send it to S3. This github python package can then be accessed in the same way you would the local zip packages. For example if the `github_zip_urls.txt` file had a single line `https://github.com/moj-analytical-services/gluejobutils/archive/master.zip`. The package `gluejobutils` would be accessible in the `job.py` script:\n\n```python\nfrom gluejobutils.s3 import read_json_from_s3\n```\n\n#### shared_job_resources folder\n\nThis a specific folder (must have the name `shared_job_resources`). This folder has the same structure and restrictions as a normal glue job folder but does not have a `job.py` file. Instead anything in the `glue_resources` or `glue_py_resources` folders will also be used (and therefore uploaded to S3) by any other glue job. Take the example below:\n\n```\n├── glue_jobs/\n│   ├── job1/\n│   |   ├── job.py\n│   |   ├── glue_resources/\n|   |   |   └── lookup_table.csv\n│   |   └── glue_py_resources/\n|   |       └── job1_specific_functions.py\n|   |\n|   ├── shared_job_resources/\n│   |   ├── glue_resources/\n|   |   |   └── some_global_config.json\n│   |   └── glue_py_resources/\n|   |       └── utils.py\n```\n\nRunning the glue job `job1` i.e.\n\n```python\njob = GlueJob(\'glue_jobs/job1/\', bucket, job_role)\njob.run_job()\n```\n\nThis glue job would not only have access the the python script `job1_specific_functions.py` and file `lookup_table.csv` but also have access to the python script `utils.py` and file `some_global_config.json`. This is because the latter two files are in the `shared_job_resources` folder and accessible to all job folders (in their `glue_jobs` parent folder).\n\n**Note:** Users should make sure there is no naming conflicts between filenames that are uploaded to S3 as they are sent to the same working folder.\n\n### Using the Glue Job class\n\nReturning to the initial example:\n\n```python\nfrom etl_manager.etl import GlueJob\n\nmy_role = \'aws_role\'\nbucket = \'bucket-to-store-temp-glue-job-in\'\n\njob = GlueJob(\'glue_jobs/simple_etl_job/\', bucket=bucket, job_role=my_role)\n```\n\nAllows you to create a job object. The GlueJob class will have a `job_name` which is defaulted to the folder name you pointed it to i.e. in this instance the job is called `simple_etl_job`. To change the job name:\n\n```python\njob.job_name = \'new_job_name\'\n```\n\nIn AWS you can only have unique job names.\n\nOther useful function and properties:\n\n```python\n# Increase the number of workers on a glue job (default is 2)\njob.allocated_capacity = 5\n\n# Set job arguments these are input params that can be accessed by the job.py script\njob.job_arguments = {"--test_arg" : \'some_string\', "--enable-metrics" : ""}\n```\n\n####\xa0job_arguments\n\nThese are strings that can be passed to the glue job script. Below is an example of how these are accessed in the `job.py` script. This code snippit is taken from the `simple_etl_job` found in the `example` folder of this repo.\n\n```python\n# Example job tests access to all files passed to the job runner class\nimport sys\nimport os\n\nfrom awsglue.utils import getResolvedOptions\nfrom pyspark.context import SparkContext\nfrom awsglue.context import GlueContext\nfrom awsglue.job import Job\n\nfrom gluejobutils.s3 import read_json_from_s3\n\nargs = getResolvedOptions(sys.argv, [\'JOB_NAME\', \'metadata_base_path\', \'test_arg\'])\n\nprint "JOB SPECS..."\nprint "JOB_NAME: ", args["JOB_NAME"]\nprint "test argument: ", args["test_arg"]\n\n# Read in meta data json\nmeta_employees = read_json_from_s3(os.path.join(args[\'metadata_base_path\'], "employees.json"))\n\n### etc\n```\n\n>**Notes:**\n> - The `test_arg` does not have two dashes in front of it. When specifying job_arguments with the GlueJob class it must be suffixed with `--` but you should remove these when accessing the args in the `job.py` script.\n> - `metadata_base_path` is a special parameter that is set by the GlueJob class. It is the S3 path to where the `meta_data` folder is in S3 so that you can read in your agnostic metadata files if you want to use them in your glue job. Note that the [gluejobutils](https://github.com/moj-analytical-services/gluejobutils) package has a lot of functionality with integrating our metadata jsons with spark.\n> - The GlueJob argument `--enable-metrics` is also a special parameter that enables you to see metrics of your glue job. [See here for more details on enabling metrics](https://docs.aws.amazon.com/en_us/glue/latest/dg/monitor-profile-glue-job-cloudwatch-metrics.html).\n> - Note that `JOB_NAME` is a special parameter that is not set in GlueJob but automatically passed to the AWS Glue when running `job.py`. [See here for more on special parameters](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-glue-arguments.html).\n\nExample of full `glue_job` and `meta_data` structures and code can be found [here](https://github.com/moj-analytical-services/etl_manager/tree/master/example).\n\n# Unit Tests\n\nThis package has [unit tests](https://github.com/moj-analytical-services/etl_manager/blob/master/tests/test_tests.py) which can also be used to see functionality.\n\nUnit tests can be ran by:\n\n```python\npython -m unittest tests.test_tests -v\n```\n',
    'author': 'Karik Isichei',
    'author_email': 'karik.isichei@digital.justice.gov.uk',
    'maintainer': None,
    'maintainer_email': None,
    'url': None,
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.6,<4.0',
}


setup(**setup_kwargs)
