diff --git a/README.rst b/README.rst index 952883d..1beaede 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,32 @@ Advanced Features --single-order \ --template my_template.yml +2. Use a custom settings file +:: + + # 1. save and edit a copy of the default settings + $ line_item_manager show settings > my_settings.yml + + # 2. edit my_settings.yml; e.g. use a custom bidder code + + # 3. create line items referencing your custom settings + $ line_item_manager create my_config.yml \ + --single-order \ + --settings my_settings.yml + +3. Use a custom schema file +:: + + # 1. save and edit a copy of the default schema + $ line_item_manager show schema > my_schema.yml + + # 2. edit my_schema.yml; e.g. use a custom currency list + + # 3. create line items referencing your custom schema + $ line_item_manager create my_config.yml \ + --single-order \ + --schema my_schema.yml + Local Development ----------------- diff --git a/line_item_manager/__init__.py b/line_item_manager/__init__.py index 4ba86ee..3edb8f2 100644 --- a/line_item_manager/__init__.py +++ b/line_item_manager/__init__.py @@ -5,7 +5,7 @@ __version__ = '0.2.10' # For an official release, use dev_version = '' -dev_version = '' +dev_version = '1' version = __version__ if dev_version: diff --git a/line_item_manager/cli.py b/line_item_manager/cli.py index f1f27db..b11e9e8 100644 --- a/line_item_manager/cli.py +++ b/line_item_manager/cli.py @@ -40,6 +40,12 @@ def cli(ctx: click.core.Context, version: bool) -> None: @click.option('--template', type=click.Path(exists=True), help='Advanced users: path to custom line item template. ' \ 'Use "line_item_manager show template" to see the package default') +@click.option('--settings', + type=click.Path(exists=True), help='Advanced users: path to settings file. ' \ + 'Use "line_item_manager show settings" to see the package default') +@click.option('--schema', + type=click.Path(exists=True), help='Advanced users: path to schema file. ' \ + 'Use "line_item_manager show schema" to see the package default') @click.option('--single-order', '-s', is_flag=True, help='Create a single set of orders instead of orders per bidder.') @click.option('--bidder-code', '-b', multiple=True, @@ -154,13 +160,18 @@ def show_resource(filename: str) -> None: print(fp.read()) @cli.command() -@click.argument('resource', type=click.Choice(['config', 'bidders', 'template'])) +@click.argument('resource', type=click.Choice(['config', 'bidders', 'template', + 'settings', 'schema'])) def show(resource: str) -> None: """Show resources""" if resource == 'config': show_resource('conf.d/line_item_manager.yml') if resource == 'template': show_resource('conf.d/line_item_template.yml') + if resource == 'settings': + show_resource('conf.d/settings.yml') + if resource == 'schema': + show_resource('conf.d/schema.yml') if resource == 'bidders': print("%-25s%s" % ('Code', 'Name')) print("%-25s%s" % ('----', '----')) diff --git a/line_item_manager/config.py b/line_item_manager/config.py index 3a27e59..b09c173 100644 --- a/line_item_manager/config.py +++ b/line_item_manager/config.py @@ -18,7 +18,7 @@ class Config: def __init__(self): self._schema = None self._cpm_names = None - self._app = load_package_file('settings.yml') + self._app = None self._start_time = datetime.now() self.set_logger() @@ -42,6 +42,8 @@ def set_log_level(self) -> None: @property def app(self) -> dict: + if self._app is None: + self._app = self.settings_obj() return self._app @property @@ -88,7 +90,8 @@ def network_name(self) -> str: @property def schema(self) -> dict: if self._schema is None: - self._schema = load_package_file('schema.yml') + self._schema = load_file(self.cli['schema']) if self.cli['schema'] else \ + load_package_file('schema.yml') return self._schema def bidder_codes(self) -> List[str]: @@ -141,6 +144,11 @@ def template_src(self) -> str: return fp.read() return read_package_file('line_item_template.yml') + def settings_obj(self) -> dict: + if self.cli['settings']: + return load_file(self.cli['settings']) + return load_package_file('settings.yml') + def pre_create(self) -> None: li_ = self.user['line_item'] is_standard = li_['item_type'].upper() == "STANDARD" diff --git a/line_item_manager/operations.py b/line_item_manager/operations.py index d3404bf..6fe591a 100644 --- a/line_item_manager/operations.py +++ b/line_item_manager/operations.py @@ -116,10 +116,11 @@ class CreativeVideo(Creative): 'vastRedirectType', 'duration') def __init__(self, *args, xsi_type: str='VastRedirectCreative', vastRedirectType: str='LINEAR', - duration: int=config.app['prebid']['creative']['video']['max_duration'], **kwargs): + duration: int, **kwargs): kwargs['xsi_type'] = xsi_type kwargs['vastRedirectType'] = vastRedirectType - kwargs['duration'] = duration + kwargs['duration'] = duration if duration else \ + config.app['prebid']['creative']['video']['max_duration'] super().__init__(*args, **kwargs) class CreativeBanner(Creative): diff --git a/tests/conftest.py b/tests/conftest.py index ad6c0fb..72945c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,10 @@ def pytest_configure(): @pytest.fixture def cli_config(request): + config._app = None + config._cpm_names = None + config._schema = None + # patch cli_str = request.node.get_closest_marker('command').args[0] cli_args = shlex.split(cli_str) diff --git a/tests/resources/li_template.yml b/tests/resources/li_template.yml index aff9a0e..b0cce26 100644 --- a/tests/resources/li_template.yml +++ b/tests/resources/li_template.yml @@ -1,3 +1,4 @@ +# custom template file orderId: {{ li.order.id }} name: "{{ li_cfg.name }}" diff --git a/tests/resources/schema.yml b/tests/resources/schema.yml new file mode 100644 index 0000000..1ef3c2a --- /dev/null +++ b/tests/resources/schema.yml @@ -0,0 +1,305 @@ +$schema: "http://json-schema.org/draft-07/schema#" +title: "Prebid Line Item Manager Schema" +type: "object" +additionalProperties: False +properties: + publisher: + type: "object" + additionalProperties: False + properties: + network_code: + $ref: "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" + network_name: + type: "string" + bidder_key_map: + type: "object" + creative: + type: "object" + additionalProperties: False + properties: + name: + type: "string" + video: + type: "object" + additionalProperties: False + properties: + sizes: + $ref: "#/definitions/sizeArray" + vast_xml_url: + type: "string" + duration: + $ref: "#/definitions/positiveIntegerType" + max_duration: + $ref: "#/definitions/positiveIntegerType" + size_override: + type: "boolean" + required: + - "sizes" + - "vast_xml_url" + banner: + type: "object" + additionalProperties: False + properties: + sizes: + $ref: "#/definitions/sizeArray" + snippet: + type: "string" + safe_frame: + type: "boolean" + size_override: + type: "boolean" + required: + - "sizes" + - "snippet" + allOf: + - required: + - "name" + - anyOf: + - required: + - "video" + - required: + - "banner" + advertiser: + type: "object" + additionalProperties: False + properties: + id: + type: "number" + name: + type: "string" + type: + $ref: "#/definitions/companyType" + anyOf: + - required: + - "id" + - required: + - "name" + order: + type: "object" + additionalProperties: False + properties: + name: + type: "string" + appliedTeamIds: + type: "array" + minItems: 1 + items: + type: "number" + required: + - "name" + line_item: + type: "object" + additionalProperties: False + properties: + name: + type: "string" + item_type: + enum: + - "test_price_priority" + - "standard" + - "sponsorship" + priority: + $ref: "#/definitions/priorityType" + start_datetime: + type: "string" + format: "date-time" + end_datetime: + type: "string" + format: "date-time" + timezone: + type: "string" + goal: + type: "object" + additionalProperties: False + properties: + units: + type: "number" + goalType: + type: "string" + unitType: + type: "string" + required: + - "units" + if: + properties: + item_type: + const: + "standard" + required: + - "name" + - "item_type" + then: + required: + - "end_datetime" + targeting: + type: "object" + additionalProperties: False + properties: + bidder: + type: "object" + additionalProperties: False + properties: + reportableType: + $ref: "#/definitions/targetingReportableType" + custom: + type: "array" + minItems: 1 + items: + type: "object" + additionalProperties: False + properties: + name: + type: "string" + operator: + $ref: "#/definitions/targetingOperatorType" + values: + $ref: "http://json-schema.org/draft-07/schema#/definitions/stringArray" + reportableType: + $ref: "#/definitions/targetingReportableType" + required: + - name + - values + placement_names: + $ref: "http://json-schema.org/draft-07/schema#/definitions/stringArray" + ad_unit_names: + $ref: "http://json-schema.org/draft-07/schema#/definitions/stringArray" + rate: + type: "object" + additionalProperties: False + properties: + currency: + $ref: "#/definitions/rateCurrencyType" + granularity: + $ref: "#/definitions/granularityType" + vcpm: + $ref: "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" + required: + - "currency" + - "granularity" +required: + - "advertiser" + - "order" + - "line_item" + - "creative" + - "rate" +definitions: + cpmBucketArray: + type: "array" + minItems: 1 + items: + $ref: "#/definitions/cpmBucketType" + cpmBucketType: + type: "object" + additionalProperties: False + properties: + min: + $ref: "#/definitions/cpmType" + max: + $ref: "#/definitions/cpmType" + interval: + $ref: "#/definitions/cpmType" + required: + - "min" + - "max" + - "interval" + cpmType: + type: "number" + minimum: 0.01 + granularityType: + type: "object" + additionalProperties: False + properties: + type: + $ref: "#/definitions/granularityPredefinedType" + custom: + $ref: "#/definitions/cpmBucketArray" + if: + properties: + type: + const: + "custom" + required: + - "type" + then: + required: + - "custom" + granularityPredefinedType: + enum: + - 'low' + - 'med' + - 'high' + - 'auto' + - 'dense' + - 'custom' + positiveIntegerType: + type: "integer" + minimum: 1 + priorityType: + type: "integer" + minimum: 1 + maximum: 16 + targetingOperatorType: + enum: + - 'IS' + - 'IS_NOT' + targetingReportableType: + enum: + - 'ON' + - 'OFF' + - 'CUSTOM_DIMENSION' + companyType: + enum: + - 'AD_NETWORK' + - 'ADVERTISER' + rateCurrencyType: + enum: + - 'AUD' + - 'BGN' + - 'BRL' + - 'CAD' + - 'CHF' + - 'CNY' + - 'CZK' + - 'DKK' + - 'EUR' + - 'GBP' + - 'HKD' + - 'HRK' + - 'HUF' + - 'IDR' + - 'ILS' + - 'INR' + - 'ISK' + - 'JPY' + - 'KRW' + - 'MXN' + - 'MYR' + - 'NOK' + - 'NZD' + - 'PHP' + - 'PLN' + - 'RON' + - 'RUB' + - 'SEK' + - 'SGD' + - 'THB' + - 'TRY' + - 'USD' + - 'ZAR' + sizeArray: + type: "array" + minItems: 1 + items: + $ref: "#/definitions/sizeType" + sizeType: + type: "object" + additionalProperties: False + properties: + height: + $ref: "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" + width: + $ref: "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger" + required: + - "height" + - "width" + diff --git a/tests/resources/settings.yml b/tests/resources/settings.yml new file mode 100644 index 0000000..06c6ea8 --- /dev/null +++ b/tests/resources/settings.yml @@ -0,0 +1,84 @@ +mgr: + advertiser: + type: "TEST_ADVERTISER" + creative: + banner: + size_override: True + video: + size_override: False + size_override: + name_template: '{{ name }} Copy:{{ index }}' + date_fmt: "%m/%d/%y %H:%M" + dry_run: + id_prefix: 9999 + test_run: + line_item_limit: 2 + timezone: "UTC" + max_lica_records: 100 + +googleads: + version: 'v202308' + line_items: + micro_cent_factor: 1000000 + max_per_order: 450 + +prebid: + bidders: + data: https://docs.prebid.org/dev-docs/bidder-data.csv + key_char_limit: 20 + keys: + - "hb_pb" + - "hb_bidder" + - "hb_adid" + - "hb_size" + - "hb_source" + - "hb_format" + - "hb_cache_host" + - "hb_cache_id" + - "hb_uuid" + - "hb_cache_path" + - "hb_deal" + single_order: + code: "hb" + name: "Top Bid" + targeting_key: "hb_pb" + creative: + size_override: + height: 1 + width: 1 + video: + max_duration: 30000 # milliseconds + price_granularity: + low: + - min: 0.50 + max: 5.00 + interval: 0.50 + med: + - min: 0.10 + max: 20.00 + interval: 0.10 + high: + - min: 0.01 + max: 20.00 + interval: 0.01 + auto: + - min: 0.05 + max: 5.00 + interval: 0.05 + - min: 5.10 + max: 10.00 + interval: 0.10 + - min: 10.50 + max: 20.00 + interval: 0.50 + dense: + - min: 0.01 + max: 3.00 + interval: 0.01 + - min: 3.05 + max: 8.00 + interval: 0.05 + - min: 8.50 + max: 20.00 + interval: 0.50 + diff --git a/tests/test_config.py b/tests/test_config.py index 490ff26..b771b35 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,11 +5,13 @@ from line_item_manager.config import config, VERBOSE1, VERBOSE2 from line_item_manager.prebid import PrebidBidder, prebid -from line_item_manager.utils import package_filename +from line_item_manager.utils import package_filename, load_file CONFIG_FILE = 'tests/resources/cfg.yml' KEY_FILE = 'tests/resources/gam_creds.json' TMPL_FILE = 'tests/resources/li_template.yml' +SETTINGS_FILE = 'tests/resources/settings.yml' +SCHEMA_FILE = 'tests/resources/schema.yml' CONFIG_BIDDER = list(prebid.bidders.keys())[0] config._start_time = pytest.start_time @@ -32,6 +34,8 @@ def test_bidders(cli_config): assert config.custom_targeting_key_values() == \ [{'name': 'country', 'values': {'CAN', 'US'}, 'operator': 'IS', 'reportableType': 'OFF'}] assert config.template_src() == open(package_filename('line_item_template.yml')).read() + assert config.settings_obj() == load_file(package_filename('settings.yml')) + assert config.schema == load_file(package_filename('schema.yml')) @pytest.mark.command(f'create {CONFIG_FILE} -k {KEY_FILE} --single-order') def test_single_order(cli_config): @@ -54,6 +58,14 @@ def test_test_run(cli_config): def test_template(cli_config): assert config.template_src() == open(TMPL_FILE).read() +@pytest.mark.command(f'create {CONFIG_FILE} -k {KEY_FILE} -b {CONFIG_BIDDER} -b ix --settings {SETTINGS_FILE}') +def test_settings(cli_config): + assert config.settings_obj() == load_file(SETTINGS_FILE) + +@pytest.mark.command(f'create {CONFIG_FILE} -k {KEY_FILE} -b {CONFIG_BIDDER} -b ix --schema {SCHEMA_FILE}') +def test_schema(cli_config): + assert config.schema == load_file(SCHEMA_FILE) + @pytest.mark.command(f'create {CONFIG_FILE} -k {KEY_FILE} --network-code 9876 --network-name abcd -b {CONFIG_BIDDER} -b ix') def test_network_meta(cli_config): assert config.network_code == 9876