Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Money data type added. #324

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
15 changes: 15 additions & 0 deletions spinta/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from spinta.types.datatype import Time
from spinta.types.datatype import DateTime
from spinta.types.datatype import File
from spinta.types.money.components import Money
from spinta.types.datatype import JSON
from spinta.types.datatype import Number
from spinta.types.datatype import Object
Expand Down Expand Up @@ -551,6 +552,20 @@ def prepare_dtype_for_response(
return dtype.default


@commands.prepare_dtype_for_response.register(Context, Format, Money, decimal.Decimal)
def prepare_dtype_for_response(
context: Context,
fmt: Format,
dtype: Money,
value: decimal.Decimal,
*,
data: Dict[str, Any],
action: Action,
select: dict = None,
):
return float(value)


@commands.prepare_dtype_for_response.register(Context, Format, File, NotAvailable)
def prepare_dtype_for_response(
context: Context,
Expand Down
19 changes: 17 additions & 2 deletions spinta/backends/fs/commands/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from spinta import commands
from spinta.components import Context, Action, Property, DataItem
from spinta.backends.components import Backend
from spinta.types.datatype import DataType, File
from spinta.types.datatype import DataType, File, Money
from spinta.backends.fs.components import FileSystem
from spinta.exceptions import FileNotFound, ConflictingValue, UnacceptableFileName
from spinta.exceptions import FileNotFound, ConflictingValue, UnacceptableFileName, InvalidValue


@commands.simple_data_check.register(Context, DataItem, File, Property, FileSystem, dict)
Expand All @@ -23,6 +23,21 @@ def simple_data_check(
_validate_path(value['_id'], backend, dtype)


@commands.simple_data_check.register(Context, DataItem, Money, Property, FileSystem, dict)
def simple_data_check(
context: Context,
data: DataItem,
dtype: Money,
prop: Property,
backend: FileSystem,
value: dict,
):
currency = data.given.get('currency')
if currency is not None:
if len(currency) > 3:
raise InvalidValue(dtype, value=currency)


@commands.complex_data_check.register()
def complex_data_check(
context: Context,
Expand Down
Empty file.
24 changes: 24 additions & 0 deletions spinta/backends/postgresql/types/money/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import sqlalchemy as sa

from spinta import commands
from spinta.backends.postgresql.components import PostgreSQL
from spinta.backends.postgresql.helpers import get_column_name
from spinta.components import Context
from spinta.types.money.components import Money


@commands.prepare.register(Context, PostgreSQL, Money)
def prepare(
context: Context,
backend: PostgreSQL,
dtype: Money,
):
prop = dtype.prop

name = get_column_name(prop)

columns = [
sa.Column(name, sa.Numeric),
sa.Column('currency', sa.String)
Gediminas-msc marked this conversation as resolved.
Show resolved Hide resolved
]
return columns
1 change: 1 addition & 0 deletions spinta/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
'image': 'spinta.types.datatype:Image',
'geometry': 'spinta.types.geometry.components:Geometry',
'spatial': 'spinta.types.geometry.components:Spatial',
'money': 'spinta.types.money.components:Money',
'ref': 'spinta.types.datatype:Ref',
'backref': 'spinta.types.datatype:BackRef',
'generic': 'spinta.types.datatype:Generic',
Expand Down
4 changes: 4 additions & 0 deletions spinta/types/datatype.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ class URI(String):
pass


class Money(DataType):
pass


class Ref(DataType):
# Referenced model
model: Model
Expand Down
4 changes: 4 additions & 0 deletions spinta/types/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
from spinta.types.namespace import load_namespace_from_name
from spinta.units.helpers import is_unit
from spinta.utils.schema import NA
from spinta.types.money.components import Money


if TYPE_CHECKING:
from spinta.datasets.components import Attribute
Expand Down Expand Up @@ -263,6 +265,8 @@ def _link_prop_enum(
for enums_ in enums:
if enums_ and prop.given.enum in enums_:
return enums_[prop.given.enum]
if isinstance(prop.dtype, Money):
return
if not is_unit(prop.dtype, prop.given.enum):
raise UndefinedEnum(prop, name=prop.given.enum)
elif prop.enums:
Expand Down
Empty file added spinta/types/money/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions spinta/types/money/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Optional

import decimal

from spinta.types.datatype import DataType


class Money(DataType):
amount: Optional[decimal.Decimal] = None # Money amount
currency: Optional[str] = None # Currency (tree letter code).
15 changes: 15 additions & 0 deletions spinta/types/money/link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from spinta import commands
from spinta.components import Context
from spinta.exceptions import UndefinedEnum
from spinta.types.money.components import Money

from spinta.types.helpers import set_dtype_backend


@commands.link.register(Context, Money)
def link(context: Context, dtype: Money) -> None:
set_dtype_backend(dtype)
enum = dtype.prop.given.enum
if enum:
if len(enum) > 3:
raise(UndefinedEnum(dtype.prop, name=enum))
128 changes: 128 additions & 0 deletions tests/dtypes/test_money.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from typing import cast

from pytest import FixtureRequest

from spinta.backends.postgresql.components import PostgreSQL
from spinta.commands.write import dataitem_from_payload
from spinta.core.config import RawConfig
from spinta.testing.client import create_test_client
from spinta.testing.data import listdata
from spinta.testing.manifest import bootstrap_manifest

import pytest

from spinta import commands
from spinta.manifests.components import Manifest
from spinta.components import Store


def test_money(
rc: RawConfig,
postgresql: str,
request: FixtureRequest,
):
context = bootstrap_manifest(rc, '''
d | r | b | m | property | type | ref
backends/postgres/dtypes/money | |
| | | City | |
| | | | name | string |
| | | | amount | money | EUR
''', backend=postgresql, request=request)

model: str = 'backends/postgres/dtypes/money/City'

app = create_test_client(context)
app.authmodel(model, [
'insert',
'getall',
])

# Write data
resp = app.post(f'/{model}', json={
'name': "Vilnius",
'amount': 100
Gediminas-msc marked this conversation as resolved.
Show resolved Hide resolved
})
assert resp.status_code == 201

# Read data
resp = app.get(f'/{model}')
assert listdata(resp, full=True) == [
{
'name': "Vilnius",
'amount': 100
Gediminas-msc marked this conversation as resolved.
Show resolved Hide resolved
}
]
Gediminas-msc marked this conversation as resolved.
Show resolved Hide resolved


def test_money_currency(
rc: RawConfig,
postgresql: str,
request: FixtureRequest,
):
context = bootstrap_manifest(rc, '''
d | r | b | m | property | type | ref
backends/postgres/dtypes/money | |
| | | City | |
| | | | name | string |
| | | | amount | money |
| | | | currency | string |
Gediminas-msc marked this conversation as resolved.
Show resolved Hide resolved
''', backend=postgresql, request=request)

model: str = 'backends/postgres/dtypes/money/City'

app = create_test_client(context)
app.authmodel(model, [
'insert',
'getall',
])

# Write data
resp = app.post(f'/{model}', json={
'name': "Vilnius",
'amount': 100,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ref is not given, then full form ({'_amount': 100, '_currency': 'EUR'}) should be given.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For write:
spinta/backends/postgesql/commands/write.py def insert() -> patch (money, money._currency, money._amount)

'currency': 'EUR',
})
assert resp.status_code == 201

# Read data
resp = app.get(f'/{model}')
assert listdata(resp, full=True) == [
{
'name': "Vilnius",
'amount': 100,
'currency': 'EUR',
Gediminas-msc marked this conversation as resolved.
Show resolved Hide resolved
}
]


@pytest.mark.parametrize('value', [
'EUR',
'EEUR',
])
def test_currency_value(rc: RawConfig, postgresql: str, value: str):
context = bootstrap_manifest(rc, '''
d | r | b | m | property | type | ref
backends/postgres/dtypes/money | |
| | | City | |
| | | | name | string |
| | | | amount | money |
| | | | currency | string |
Comment on lines +108 to +109
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| | | | amount | money |
| | | | currency | string |
| | | | amount | money | {value}
| | | | amount._currency | string |

Currency should be set as ref.

''', backend=postgresql)

store: Store = context.get('store')
manifest: Manifest = store.manifest

model = manifest.models['backends/postgres/dtypes/money/City']
backend = model.backend

load_data_for_payload = {
'_op': 'insert',
'currency': value,
}

context.set('transaction', backend.transaction(write=True))

data = dataitem_from_payload(context, model, load_data_for_payload)
data.given = commands.load(context, model, data.payload)

commands.simple_data_check(context, data, model, backend)