chartout 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: chartout
3
+ Version: 0.0.0
4
+ Summary: Chartout - Print Your Insights, Anytime, Anywhere
5
+ Home-page: https://chartout.io
6
+ Download-URL: https://download_url.com
7
+ Author: mattijn
8
+ Author-email: info@hoekinsights.nl
9
+ License: GPL-3.0 license
10
+ Project-URL: Source, https://github.com/hoekinsights/chartout
11
+ Keywords: python3 python reserve reserver reserved
12
+ Classifier: Development Status :: 1 - Planning
13
+ Classifier: Programming Language :: Python :: 3.6
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.6
21
+ Description-Content-Type: text/markdown
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: download-url
28
+ Dynamic: home-page
29
+ Dynamic: keywords
30
+ Dynamic: license
31
+ Dynamic: project-url
32
+ Dynamic: requires-python
33
+ Dynamic: summary
34
+
35
+
36
+ ## Overview
37
+ chartout is a Python library for doing awesome things.
38
+ This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
@@ -0,0 +1,4 @@
1
+
2
+ ## Overview
3
+ chartout is a Python library for doing awesome things.
4
+ This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """chartout modules."""
chartout-0.0.0/cart.py ADDED
@@ -0,0 +1,84 @@
1
+ from dataclasses import dataclass, asdict
2
+ from typing import Optional, List
3
+ from .models import CartItem
4
+ from .support import viz_to_cart_item, VizLike
5
+
6
+
7
+ @dataclass
8
+ class Cart:
9
+ """A class to represent a shopping cart containing CartItems.
10
+
11
+ Attributes:
12
+ items (List[CartItem]): A list of items in the cart, which can be
13
+ CartItem instances.
14
+ """
15
+
16
+ items: List[CartItem]
17
+
18
+ def __init__(self, items: Optional[List[CartItem]] = None):
19
+ """Initialize the Cart with an optional list of items.
20
+
21
+ Args:
22
+ items (Optional[List[CartItem]]): A list of dictionaries conforming to
23
+ CartItem to initialize the cart with.
24
+ """
25
+ self.items: List[CartItem] = []
26
+ if items is not None:
27
+ self.add(items)
28
+
29
+ def add(self, item: CartItem | List[CartItem] | VizLike) -> None:
30
+ """Add a CartItem, a list of CartItems, or a VizLike to the cart.
31
+
32
+ Args:
33
+ item (CartItem | List[CartItem] | VizLike): A CartItem, a list of CartItems, or a VizLike to be added to the cart.
34
+
35
+ Raises:
36
+ ValueError: If an item in the list is not a valid CartItem or VizLike.
37
+ """
38
+ if isinstance(item, VizLike):
39
+ # Convert VizLike to CartItem
40
+ item = viz_to_cart_item(item)
41
+ self.items.append(item)
42
+ elif isinstance(item, list):
43
+ for i in item:
44
+ if isinstance(i, dict):
45
+ i = CartItem(**i)
46
+ self.items.append(i)
47
+ else:
48
+ if isinstance(item, dict):
49
+ item = CartItem(**item)
50
+ self.items.append(item)
51
+
52
+ def remove(self, *, index: int) -> None:
53
+ """Remove a CartItem from the cart by its index.
54
+
55
+ Args:
56
+ index (int): The index of the item to be removed from the cart.
57
+
58
+ Raises:
59
+ IndexError: If the index is out of range.
60
+ """
61
+ if index < 0 or index >= len(self.items):
62
+ raise IndexError("Index out of range.")
63
+ del self.items[index]
64
+
65
+ def __repr__(self) -> str:
66
+ """Return a string representation of the Cart.
67
+
68
+ Returns:
69
+ str: A string representation of the Cart, including item IDs, names, quantities, and texture information.
70
+ """
71
+ if not self.items:
72
+ return "Cart(empty)"
73
+
74
+ items_repr = "\n".join(
75
+ f" - ID: {item.id}\n"
76
+ f" Name: {item.name or 'Unnamed'}\n"
77
+ f" Quantity: {item.quantity}\n"
78
+ f" Textures: [{', '.join(texture.id for texture in item.textures)}]"
79
+ for item in self.items
80
+ )
81
+ return f"Cart:\n{items_repr}"
82
+
83
+ def to_dict(self):
84
+ return asdict(self)
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: chartout
3
+ Version: 0.0.0
4
+ Summary: Chartout - Print Your Insights, Anytime, Anywhere
5
+ Home-page: https://chartout.io
6
+ Download-URL: https://download_url.com
7
+ Author: mattijn
8
+ Author-email: info@hoekinsights.nl
9
+ License: GPL-3.0 license
10
+ Project-URL: Source, https://github.com/hoekinsights/chartout
11
+ Keywords: python3 python reserve reserver reserved
12
+ Classifier: Development Status :: 1 - Planning
13
+ Classifier: Programming Language :: Python :: 3.6
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.6
21
+ Description-Content-Type: text/markdown
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: download-url
28
+ Dynamic: home-page
29
+ Dynamic: keywords
30
+ Dynamic: license
31
+ Dynamic: project-url
32
+ Dynamic: requires-python
33
+ Dynamic: summary
34
+
35
+
36
+ ## Overview
37
+ chartout is a Python library for doing awesome things.
38
+ This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
@@ -0,0 +1,14 @@
1
+ README.md
2
+ __init__.py
3
+ cart.py
4
+ develop.py
5
+ models.py
6
+ py.typed
7
+ setup.py
8
+ store.py
9
+ support.py
10
+ texture.py
11
+ chartout.egg-info/PKG-INFO
12
+ chartout.egg-info/SOURCES.txt
13
+ chartout.egg-info/dependency_links.txt
14
+ chartout.egg-info/top_level.txt
@@ -0,0 +1,120 @@
1
+ def altair_heatmap():
2
+ import altair as alt
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ # Compute x^2 + y^2 across a 2D grid
7
+ x, y = np.meshgrid(range(-5, 5), range(-5, 5))
8
+ z = x**2 + y**2
9
+
10
+ # Convert this grid to columnar data expected by Altair
11
+ source = pd.DataFrame({"x": x.ravel(), "y": y.ravel(), "z": z.ravel()})
12
+
13
+ chart = alt.Chart(source).mark_rect().encode(x="x:O", y="y:O", color="z:Q")
14
+ return chart
15
+
16
+ def altair_comet():
17
+ import altair as alt
18
+ import vega_datasets
19
+
20
+ chart = alt.Chart(
21
+ vega_datasets.data.barley.url,
22
+ title='Barley Yield comparison between 1932 and 1931'
23
+ ).mark_trail().encode(
24
+ alt.X('year:O').title(None),
25
+ alt.Y('variety:N').title('Variety'),
26
+ alt.Size('yield:Q')
27
+ .scale(range=[0, 12])
28
+ .legend(values=[20, 60])
29
+ .title('Barley Yield (bushels/acre)'),
30
+ alt.Color('delta:Q')
31
+ .scale(domainMid=0)
32
+ .title('Yield Delta (%)'),
33
+ alt.Tooltip(['year:O', 'yield:Q']),
34
+ alt.Column('site:N').title('Site')
35
+ ).transform_pivot(
36
+ "year",
37
+ value="yield",
38
+ groupby=["variety", "site"]
39
+ ).transform_fold(
40
+ ["1931", "1932"],
41
+ as_=["year", "yield"]
42
+ ).transform_calculate(
43
+ calculate="datum['1932'] - datum['1931']",
44
+ as_="delta"
45
+ ).configure_legend(
46
+ orient='bottom',
47
+ direction='horizontal'
48
+ ).configure_view(
49
+ stroke=None
50
+ )
51
+ return chart
52
+
53
+ def altair_map_selection():
54
+ import altair as alt
55
+ from vega_datasets import data
56
+ import geopandas as gpd
57
+
58
+ # load data
59
+ gdf_quakies = gpd.read_file(data.earthquakes.url, driver="GeoJSON")
60
+ gdf_world = gpd.read_file(data.world_110m.url, driver="TopoJSON")
61
+
62
+ # defintion for interactive brush
63
+ brush = alt.selection_interval(
64
+ encodings=["longitude"], empty=False, value={"longitude": [-50, -110]}
65
+ )
66
+
67
+ # world disk
68
+ sphere = alt.Chart(alt.sphere()).mark_geoshape(
69
+ fill="transparent", stroke="lightgray", strokeWidth=1
70
+ )
71
+
72
+ # countries as shapes
73
+ world = alt.Chart(gdf_world).mark_geoshape(
74
+ fill="lightgray", stroke="white", strokeWidth=0.1
75
+ )
76
+
77
+ # earthquakes as dots on map
78
+ quakes = (
79
+ alt.Chart(gdf_quakies)
80
+ .transform_calculate(
81
+ lon="datum.geometry.coordinates[0]",
82
+ lat="datum.geometry.coordinates[1]",
83
+ )
84
+ .mark_circle(opacity=0.35, tooltip=True)
85
+ .encode(
86
+ longitude="lon:Q",
87
+ latitude="lat:Q",
88
+ color=alt.when(brush)
89
+ .then(alt.value("goldenrod"))
90
+ .otherwise(alt.value("steelblue")),
91
+ size=alt.Size("mag:Q").scale(
92
+ type="pow", range=[1, 1000], domain=[0, 7], exponent=4
93
+ ),
94
+ )
95
+ .add_params(brush)
96
+ )
97
+
98
+ # combine layers for the map
99
+ left_map = alt.layer(sphere, world, quakes).project(type="mercator")
100
+
101
+ # histogram of binned earthquakes
102
+ bars = (
103
+ alt.Chart(gdf_quakies)
104
+ .mark_bar()
105
+ .encode(
106
+ x=alt.X("mag:Q").bin(extent=[0, 7]),
107
+ y="count(mag):Q",
108
+ color=alt.value("steelblue"),
109
+ )
110
+ )
111
+
112
+ # filtered earthquakes
113
+ bars_overlay = bars.encode(color=alt.value("goldenrod")).transform_filter(brush)
114
+
115
+ # combine layers for histogram
116
+ right_bars = alt.layer(bars, bars_overlay)
117
+
118
+ # vertical concatenate map and bars
119
+ chart = left_map | right_bars
120
+ return chart
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass, field, asdict
2
+ from typing import List, Union, Optional, Dict, Any
3
+
4
+ # Data Classes
5
+ @dataclass
6
+ class Texture:
7
+ id: str
8
+ content: Union[str, Any] # 'Any' can be used for alt.Chart or other types
9
+
10
+ def to_dict(self) -> Dict[str, Any]:
11
+ """Convert the Texture instance to a dictionary."""
12
+ return asdict(self)
13
+
14
+ @dataclass
15
+ class StoreItem:
16
+ id: str
17
+ name: Optional[str] = None
18
+ textures: List[Texture] = field(default_factory=list)
19
+
20
+ def to_dict(self) -> Dict[str, Any]:
21
+ """Convert the StoreItem instance to a dictionary."""
22
+ return asdict(self)
23
+
24
+ @dataclass
25
+ class CartItem(StoreItem):
26
+ quantity: int = 1
27
+
28
+ def to_dict(self) -> Dict[str, Any]:
29
+ """Convert the CartItem instance to a dictionary."""
30
+ return asdict(self)
31
+
32
+ @dataclass
33
+ class ActiveItem(StoreItem):
34
+ textures: List[Texture] = field(default_factory=list)
35
+
36
+ def to_dict(self) -> Dict[str, Any]:
37
+ """Convert the ActiveItem instance to a dictionary."""
38
+ return asdict(self)
39
+
40
+ @dataclass
41
+ class ActiveTexture:
42
+ texture: bytes # Equivalent to Uint8Array in TypeScript
43
+
44
+ def to_dict(self) -> Dict[str, Any]:
45
+ """Convert the ActiveTexture instance to a dictionary."""
46
+ return asdict(self)
47
+
48
+ @dataclass
49
+ class InitViz:
50
+ images: Dict[int, bytes] = field(default_factory=dict)
51
+
52
+ def to_dict(self) -> Dict[int, bytes]:
53
+ """Convert the InitViz instance to a dictionary."""
54
+ return self.images
File without changes
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,47 @@
1
+
2
+ import sys
3
+
4
+ try:
5
+ from setuptools import setup
6
+ except ImportError:
7
+ from distutils.core import setup
8
+
9
+ # invalid email
10
+ # download url
11
+ # url
12
+ # project urls
13
+
14
+ setup(
15
+ name ="chartout",
16
+ packages=[".",],
17
+ version='0.0.0',
18
+ description="Chartout - Print Your Insights, Anytime, Anywhere",
19
+ long_description="""
20
+ ## Overview
21
+ chartout is a Python library for doing awesome things.
22
+ This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
23
+ """,
24
+ long_description_content_type='text/markdown',
25
+ author="mattijn",
26
+ author_email="info@hoekinsights.nl",
27
+ url="https://chartout.io",
28
+ download_url="https://download_url.com",
29
+ keywords="python3 python reserve reserver reserved",
30
+ project_urls={
31
+ 'Source':"https://github.com/hoekinsights/chartout",
32
+ },
33
+ install_requires="",
34
+ python_requires='>=3.6',
35
+ classifiers=[
36
+ 'Development Status :: 1 - Planning',
37
+ 'Programming Language :: Python :: 3.6',
38
+ 'Programming Language :: Python :: 3.7',
39
+ 'Programming Language :: Python :: 3.8',
40
+ 'Programming Language :: Python :: 3.9',
41
+ 'Programming Language :: Python :: 3.10',
42
+ 'Programming Language :: Python :: 3.11',
43
+ 'Programming Language :: Python :: 3.12',
44
+ ],
45
+ license="GPL-3.0 license",
46
+ )
47
+
@@ -0,0 +1,135 @@
1
+ import json
2
+ import pathlib
3
+ import urllib.request
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ import anywidget
7
+ import traitlets
8
+ from traitlets import TraitType
9
+
10
+ from .cart import Cart
11
+ from .models import ActiveItem, CartItem, InitViz
12
+ from .support import (
13
+ VizLike,
14
+ is_viz_like,
15
+ viz_to_active_item,
16
+ viz_to_init_viz,
17
+ cart_item_to_active_item,
18
+ )
19
+
20
+
21
+ # Helper Classes
22
+ class MemoryViewTrait(TraitType):
23
+ """A trait type that accepts memoryview objects and converts them to bytes."""
24
+
25
+ def validate(self, obj, value):
26
+ if isinstance(value, memoryview):
27
+ return bytes(value)
28
+ elif isinstance(value, (bytes, bytearray)):
29
+ return value
30
+ else:
31
+ self.error(obj, value)
32
+
33
+
34
+ # Main Classes
35
+ class Store(anywidget.AnyWidget):
36
+ """A class representing a store widget for managing cart items.
37
+
38
+ This class provides functionality to manage the state of a shopping cart,
39
+ including adding, removing, and serializing cart items. It uses traitlets
40
+ for automatic syncing with the front-end and supports JSON serialization
41
+ for easy data interchange.
42
+
43
+ Attributes:
44
+ cart (Cart): An instance of the Cart class containing items currently in the cart.
45
+ active (Dict[str, Any]): A dictionary representing the active item state.
46
+ init (Dict[str, Any]): A read-only dictionary representing the initial
47
+ state of the store.
48
+ """
49
+
50
+ # Paths for JavaScript and CSS
51
+ _esm = pathlib.Path("../chartout-app/bundle/Widget.js")
52
+ # _css = pathlib.Path("../chartout-app/bundle/styles.css")
53
+
54
+ cart = traitlets.List(
55
+ trait=traitlets.Dict(
56
+ key_trait=traitlets.Unicode(), value_trait=traitlets.Any()
57
+ ),
58
+ allow_none=True,
59
+ default_value=None,
60
+ ).tag(sync=True)
61
+
62
+ active_item = traitlets.Dict(
63
+ key_trait=traitlets.Unicode(), value_trait=traitlets.Any(), allow_none=True
64
+ ).tag(sync=True)
65
+
66
+ active_texture = traitlets.Dict(
67
+ key_trait=traitlets.Unicode(),
68
+ value_trait=MemoryViewTrait(),
69
+ allow_none=True,
70
+ default_value=None,
71
+ read_only=False,
72
+ ).tag(sync=True)
73
+
74
+ init_viz = traitlets.Dict(
75
+ key_trait=traitlets.Int(), value_trait=traitlets.Bytes(), allow_none=True
76
+ ).tag(sync=True)
77
+
78
+ def __init__(self, item: Optional[Union[Cart, VizLike]] = None, **kwargs):
79
+ """Initialize the Store with an optional cart or a valid Altair chart."""
80
+ super().__init__(**kwargs)
81
+ self.active_texture = None
82
+ if isinstance(item, Cart):
83
+ self.cart = item.to_dict()["items"]
84
+ self.init_viz = {}
85
+ self.active_item = (
86
+ cart_item_to_active_item(self.cart[0]) if len(self.cart) > 0 else None
87
+ )
88
+ elif is_viz_like(item):
89
+ self.cart = []
90
+ init_viz_obj = viz_to_init_viz(item)
91
+ self.init_viz = init_viz_obj.to_dict()
92
+ self.active_item = viz_to_active_item(init_viz_obj).to_dict()
93
+ elif item is not None:
94
+ raise TypeError("item must be of type Cart or VizLike.")
95
+ else:
96
+ self.cart = []
97
+ self.init_viz = {}
98
+ self.active_item = None
99
+
100
+ def to_json(self):
101
+ """Serialize the widget's state to a JSON-compatible dictionary."""
102
+ return {
103
+ "cart": self.cart,
104
+ "active_item": self.active_item,
105
+ "init_viz": self.init_viz,
106
+ }
107
+
108
+ def from_json(self, data: Dict[str, Any]):
109
+ """Deserialize JSON data into the Store."""
110
+ if "cart" in data:
111
+ self.cart = Cart() # Initialize a new Cart instance
112
+ self.cart.items = [CartItem(**item) for item in data["cart"]]
113
+ if "active" in data:
114
+ self.active_item = ActiveItem(**data["active"])
115
+ if "init_viz" in data:
116
+ self.init_viz = InitViz(images=data["init_viz"]).to_dict()
117
+
118
+
119
+ # Functions
120
+ def customizables(debug: bool = False) -> Any:
121
+ """Retrieve a JSON object from the Chartout API for customizables based on category."""
122
+ if debug:
123
+ url = "http://127.0.0.1:8000/v1/products/"
124
+ else:
125
+ url = "https://api.chartout.io/v1/products/"
126
+ try:
127
+ with urllib.request.urlopen(url) as response:
128
+ if response.status != 200:
129
+ raise Exception(f"Error fetching data: {response.status}")
130
+ data = response.read()
131
+ return json.loads(data)
132
+ except urllib.error.URLError as e:
133
+ raise ConnectionError(f"Failed to connect to {url}: {e.reason}")
134
+ except json.JSONDecodeError as e:
135
+ raise ValueError(f"Failed to parse JSON response: {e.msg}")
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+ import sys
3
+ import io
4
+ from typing import TYPE_CHECKING, Any, TypeVar, Optional, Dict
5
+ import uuid
6
+ import hashlib
7
+ import os
8
+ import time
9
+
10
+ # Conditional imports for type checking
11
+ if TYPE_CHECKING:
12
+ if sys.version_info >= (3, 10):
13
+ from typing import TypeGuard
14
+ else:
15
+ from typing_extensions import TypeGuard
16
+ import altair as alt
17
+
18
+ # Import models
19
+ from .models import ActiveItem, Texture, InitViz, CartItem
20
+
21
+ # Define a new type variable for VizLike
22
+ VizLike = TypeVar("VizLike", bound=Any)
23
+
24
+
25
+ # Helper Functions
26
+ def get_altair() -> Any:
27
+ """Get altair module (if already imported - else return None)."""
28
+ return sys.modules.get("altair", None)
29
+
30
+
31
+ def is_altair_chart(chart: Any) -> TypeGuard[alt.typing.ChartType]:
32
+ """Check whether `chart` is an Altair Chart without importing altair."""
33
+ return (alt := get_altair()) is not None and alt.typing.is_chart_type(chart)
34
+
35
+
36
+ def chart_to_png(chart: Any) -> bytes:
37
+ """Convert an Altair chart to PNG byte data."""
38
+ if is_altair_chart(chart):
39
+ byte_stream = io.BytesIO()
40
+ chart.save(byte_stream, format="png", scale_factor=2, ppi=300)
41
+ byte_stream.seek(0)
42
+ return byte_stream.getvalue()
43
+ else:
44
+ msg = f"The provided DataViz object is not supported. Got: {type(chart)}"
45
+ raise TypeError(msg)
46
+
47
+
48
+ # Main Functions
49
+ def is_viz_like(viz: VizLike) -> TypeGuard[VizLike]:
50
+ """Check whether `viz` is a valid Altair chart."""
51
+ return is_altair_chart(viz)
52
+
53
+
54
+ def viz_to_active_item(init_viz: InitViz) -> ActiveItem:
55
+ """Convert an InitViz item to an ActiveItem."""
56
+ # Assuming the first image in the InitViz is used for the ActiveItem
57
+ first_image_index = next(iter(init_viz.images))
58
+ png_data = init_viz.images[first_image_index]
59
+
60
+ return ActiveItem(
61
+ name="Canvas",
62
+ id="canvas_10x10",
63
+ textures=[
64
+ Texture(
65
+ id="canvas_10x10_texture",
66
+ content=png_data
67
+ )
68
+ ]
69
+ )
70
+
71
+
72
+ def viz_to_init_viz(viz: VizLike) -> InitViz:
73
+ """Convert a VizLike item to an InitViz."""
74
+ images = {0: chart_to_png(viz)}
75
+ return InitViz(images=images)
76
+
77
+
78
+ def cart_item_to_active_item(cart_item: Dict[str, Any]) -> ActiveItem:
79
+ """Convert a CartItem to an ActiveItem."""
80
+ return ActiveItem(
81
+ id=cart_item['id'],
82
+ name=cart_item.get('name'),
83
+ textures=cart_item['textures']
84
+ )
85
+
86
+
87
+ def viz_to_cart_item(viz: VizLike) -> CartItem:
88
+ """Convert a VizLike item to a CartItem."""
89
+ # Convert the VizLike object to PNG bytes
90
+ png_data = chart_to_png(viz)
91
+
92
+ # Create a Texture instance for the CartItem
93
+ texture = Texture(
94
+ id="my_canvas_texture",
95
+ content=png_data
96
+ )
97
+
98
+ # Create and return a CartItem instance
99
+ return CartItem(
100
+ id="my_canvas_id",
101
+ name="VizLike Item",
102
+ textures=[texture],
103
+ quantity=1
104
+ )
105
+
106
+
107
+ def generate_external_id() -> str:
108
+ """Generate a unique external ID for order creation."""
109
+ # Generate a UUID
110
+ unique_id = uuid.uuid4().hex
111
+
112
+ # Get machine-specific information (e.g., hostname)
113
+ machine_info = os.uname().nodename
114
+
115
+ # Hash the machine information to keep it consistent and anonymized
116
+ machine_hash = hashlib.sha256(machine_info.encode()).hexdigest()
117
+
118
+ # Get the current timestamp
119
+ timestamp = int(time.time())
120
+
121
+ # Combine all parts to form the external_id
122
+ external_id = f"{unique_id}-{machine_hash[:8]}-{timestamp}"
123
+
124
+ return external_id
@@ -0,0 +1,172 @@
1
+ from typing import Any, Dict, Union, List
2
+ import io
3
+ from PIL import Image, ImageDraw
4
+
5
+ # Import from other modules
6
+ from .store import customizables
7
+ from .support import is_viz_like, chart_to_png
8
+
9
+ # Helper Functions
10
+ def process_image_for_source_size(img, source_size, user_modifications=None):
11
+ """Resize image to fit within source_size while maintaining aspect ratio and alignment."""
12
+ orig_width, orig_height = img.size
13
+ aspect_ratio = orig_width / orig_height
14
+
15
+ # Determine new dimensions to fit within source_size while maintaining aspect ratio
16
+ if source_size["width"] / aspect_ratio <= source_size["height"]:
17
+ fit_width = source_size["width"]
18
+ fit_height = int(fit_width / aspect_ratio)
19
+ else:
20
+ fit_height = source_size["height"]
21
+ fit_width = int(fit_height * aspect_ratio)
22
+
23
+ # Resize the image to fit within the source size
24
+ resized_img = img.resize((fit_width, fit_height))
25
+
26
+ # Apply scale modification if provided
27
+ scale = user_modifications.get("scale", 1.0) if user_modifications else 1.0
28
+ scaled_width = int(fit_width * scale)
29
+ scaled_height = int(fit_height * scale)
30
+
31
+ # Resize the image based on the scale
32
+ scaled_img = resized_img.resize((scaled_width, scaled_height))
33
+
34
+ # Create a new canvas for the source size
35
+ source_canvas = Image.new(
36
+ "RGB", (source_size["width"], source_size["height"]), (255, 255, 255)
37
+ )
38
+
39
+ # Determine alignment
40
+ alignment = user_modifications.get(
41
+ "alignment",
42
+ source_size.get("alignment", {"horizontal": "center", "vertical": "middle"}),
43
+ )
44
+ dx = user_modifications.get("dx", 0)
45
+ dy = user_modifications.get("dy", 0)
46
+
47
+ if alignment["horizontal"] == "left":
48
+ x_pos = 0 + dx
49
+ elif alignment["horizontal"] == "right":
50
+ x_pos = source_size["width"] - scaled_width + dx
51
+ else: # center
52
+ x_pos = (source_size["width"] - scaled_width) // 2 + dx
53
+
54
+ if alignment["vertical"] == "top":
55
+ y_pos = 0 + dy
56
+ elif alignment["vertical"] == "bottom":
57
+ y_pos = source_size["height"] - scaled_height + dy
58
+ else: # middle
59
+ y_pos = (source_size["height"] - scaled_height) // 2 + dy
60
+
61
+ # Paste the scaled image onto the source canvas, clipping if necessary
62
+ source_canvas.paste(scaled_img, (x_pos, y_pos))
63
+ return source_canvas, {
64
+ "scaled_width": scaled_width,
65
+ "scaled_height": scaled_height,
66
+ "x_pos": x_pos,
67
+ "y_pos": y_pos,
68
+ }
69
+
70
+ def position_image_on_canvas(resized_img, canvas_position, user_modifications):
71
+ """Position the resized image on the canvas using user modifications."""
72
+ # Create a tile canvas
73
+ tile_canvas = Image.new(
74
+ "RGB", (canvas_position["width"], canvas_position["height"]), (255, 255, 255)
75
+ )
76
+
77
+ final_img = resized_img.resize(
78
+ (canvas_position["width"], canvas_position["height"])
79
+ )
80
+
81
+ tile_canvas.paste(final_img, (0, 0))
82
+
83
+ return tile_canvas
84
+
85
+ # Main Functions
86
+ def create_tiled_image(variant):
87
+ """Create a tiled image from a variant."""
88
+ # Create a blank canvas
89
+ canvas_size = variant["canvas_size"]
90
+ canvas = Image.new("RGB", (canvas_size, canvas_size), (255, 255, 255))
91
+
92
+ for texture in variant["textures"]:
93
+ if texture["type"] == "image":
94
+ try:
95
+ # Check if content is a byte stream
96
+ if isinstance(texture["content"], bytes):
97
+ img = Image.open(io.BytesIO(texture["content"]))
98
+ else:
99
+ # Assume content is a file path
100
+ img = Image.open(texture["content"])
101
+
102
+ # Ensure the image is loaded
103
+ img.load()
104
+ except Exception as e:
105
+ print(f"Error loading image for texture ID {texture['id']}: {e}")
106
+ continue
107
+
108
+ # Process image for source size
109
+ resized_img = process_image_for_source_size(img, texture["source_size"])
110
+
111
+ # Apply user modifications and position on canvas
112
+ user_modifications = texture.get("user_modifications", {})
113
+ tile_canvas = position_image_on_canvas(
114
+ resized_img, texture["canvas_position"], user_modifications
115
+ )
116
+
117
+ # Calculate position on the main canvas
118
+ x = texture["canvas_position"]["x"]
119
+ y = texture["canvas_position"]["y"]
120
+
121
+ # Paste the processed image onto the main canvas
122
+ canvas.paste(tile_canvas, (x, y))
123
+
124
+ elif texture["type"] == "color":
125
+ # Create a colored rectangle
126
+ color = texture["content"]
127
+ x = texture["canvas_position"]["x"]
128
+ y = texture["canvas_position"]["y"]
129
+ width = texture["canvas_position"]["width"]
130
+ height = texture["canvas_position"]["height"]
131
+ draw = ImageDraw.Draw(canvas)
132
+ draw.rectangle([x, y, x + width, y + height], fill=color)
133
+
134
+ # Flip the image
135
+ canvas = canvas.transpose(Image.FLIP_TOP_BOTTOM)
136
+ # Save or display the image
137
+ output = io.BytesIO()
138
+ canvas.save(output, format="PNG")
139
+ output.seek(0)
140
+ return output.getvalue()
141
+
142
+ def variant_to_texture(id_variant: str, textures: List[Dict[str, Any]]) -> bytes:
143
+ """Create texture data image using the given variant ID and textures."""
144
+ # Retrieve product configurations from the API
145
+ products_json = customizables(debug=True)
146
+ my_variant = next(
147
+ (v for v in products_json["variants"] if v["id"] == id_variant), None
148
+ )
149
+
150
+ if not my_variant:
151
+ raise ValueError(f"Variant with ID {id_variant} not found in variants.")
152
+
153
+ # Update the variant's textures with the provided textures
154
+ for texture in textures:
155
+ for variant_texture in my_variant["textures"]:
156
+ if variant_texture["id"] == texture["id"]:
157
+ if variant_texture["type"] == "image" and is_viz_like(
158
+ texture["content"]
159
+ ):
160
+ # Convert chart to PNG if content is a chart
161
+ variant_texture["content"] = chart_to_png(texture["content"])
162
+ else:
163
+ # Directly assign content for non-chart textures
164
+ variant_texture["content"] = texture["content"]
165
+ # Update other properties if needed
166
+ variant_texture["user_modifications"] = texture.get(
167
+ "user_modifications", variant_texture.get("user_modifications", {})
168
+ )
169
+
170
+ # Create the tiled image using the updated variant
171
+ texture_data = create_tiled_image(my_variant)
172
+ return texture_data