コード例 #1
0
def save_collection(
        collection: List[str],
        session: db.Session = Depends(get_session),
        current_user: "******" = Depends(login_required),
):
    """Update the user's collection in place.

    PUT a list of release slugs to set them as the user's collection (e.g. `['master-set',
    'the-frostdale-giants']`.

    **This is not a patch!** You must pass the entire list of the user's collections every time.
    """
    # Clear out our existing releases
    session.query(UserRelease).filter(
        UserRelease.user_id == current_user.id).delete()
    session.commit()
    release_ids = ((session.query(Release.id).filter(
        Release.is_legacy.is_(False),
        Release.is_public.is_(True),
        Release.stub.in_(collection),
    ).all()) if collection else None)
    if release_ids:
        for row in release_ids:
            session.add(UserRelease(user_id=current_user.id,
                                    release_id=row.id))
        session.commit()
    query = get_releases_query(session=session, current_user=current_user)
    return query.all()
コード例 #2
0
def log_out(
        session: db.Session = Depends(get_session),
        jwt_payload: dict = Depends(get_auth_token),
        current_user: "******" = Depends(login_required),
):
    """Log a user out and revoke their JWT token's access rights.

    It's a good idea to invoke this whenever an authenticated user logs out, because tokens can otherwise be quite
    long-lived.
    """
    # Do some quick clean-up to keep our table lean and mean; deletes any tokens that expired more than 24 hours ago
    session.query(UserRevokedToken).filter(
        UserRevokedToken.expires < dt.datetime.utcnow() -
        dt.timedelta(days=1)).delete(synchronize_session=False)
    session.commit()
    # Then add our newly revoked token
    expires_at = dt.datetime.fromtimestamp(jwt_payload["exp"],
                                           tz=dt.timezone.utc)
    # No need to do `.get("jti")` here because a missing JTI would result in a credentials error in the dependencies
    revoked_hex = jwt_payload["jti"]
    revoked_uuid = uuid.UUID(hex=revoked_hex)
    revoked_token = UserRevokedToken(revoked_uuid=revoked_uuid,
                                     user_id=current_user.id,
                                     expires=expires_at)
    session.add(revoked_token)
    session.commit()
    return {"detail": "Token successfully revoked."}
コード例 #3
0
ファイル: stream.py プロジェクト: onecrayon/api.ashes.live
def refresh_stream_for_entity(session: db.Session, entity_id: int,
                              entity_type: str, source_entity_id: int):
    """Creates or updates the Stream entry for the given entity

    **Please note:** this method does not commit the changes! You must flush the session in the
    invoking method.
    """
    if entity_type == "deck":
        entity = (session.query(Stream).filter(
            Stream.source_entity_id == source_entity_id,
            Stream.entity_type == "deck",
        ).first())
    else:
        entity = session.query(Stream).filter(
            Stream.entity_id == entity_id).first()
    if not entity:
        entity = Stream(
            entity_id=entity_id,
            entity_type=entity_type,
            source_entity_id=source_entity_id,
        )
    elif entity_type == "deck":
        # Decks are a special case; we update the Stream entity because the snapshots effectively
        # replace one another as far as most users are concerned
        entity.posted = datetime.utcnow()
        entity.entity_id = entity_id
    else:
        # Ignore comment edits
        return
    session.add(entity)
コード例 #4
0
def test_get_releases(client: TestClient, session: db.Session):
    """Releases endpoint must return a list of all releases"""
    master_set = Release(name="Master Set")
    master_set.is_public = True
    session.add(master_set)
    session.commit()
    response = client.get("/v2/releases")
    assert response.status_code == status.HTTP_200_OK
    assert len(response.json()) == 1
コード例 #5
0
def test_get_releases_public_only(client: TestClient, session: db.Session):
    """Releases list must only include public releases"""
    master_set = Release(name="Master Set")
    master_set.is_public = True
    session.add(master_set)
    session.add(Release(name="Unreleased"))
    session.commit()
    response = client.get("/v2/releases")
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert len(data) == 1
    assert data[0]["stub"] == master_set.stub
コード例 #6
0
def test_patch_release_non_admin(client: TestClient, session: db.Session):
    """Patching a release must require admin access"""
    master_set = Release(name="Master Set")
    session.add(master_set)
    session.commit()
    user, token = create_user_token(session)
    response = client.patch(
        f"/v2/releases/{master_set.stub}",
        json={"is_public": True},
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == status.HTTP_403_FORBIDDEN
コード例 #7
0
def test_patch_release(client: TestClient, session: db.Session):
    """Patching a release to set it public must work"""
    master_set = Release(name="Master Set")
    session.add(master_set)
    session.commit()
    assert master_set.is_public == False
    admin, token = create_admin_token(session)
    response = client.patch(
        f"/v2/releases/{master_set.stub}",
        json={"is_public": True},
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == status.HTTP_200_OK
    session.refresh(master_set)
    assert master_set.is_public == True
コード例 #8
0
def test_put_releases_bad_release(client: TestClient, session: db.Session):
    """Putting a nonsense stub must work"""
    master_set = Release(name="Master Set")
    master_set.is_public = True
    session.add(master_set)
    session.commit()
    user, token = create_user_token(session)
    response = client.put(
        "/v2/releases/mine",
        json=["fake-set"],
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert data[0]["stub"] == master_set.stub
    assert data[0]["is_mine"] == False
コード例 #9
0
def test_release_filtration(client: TestClient, session: db.Session):
    """Filtering cards by owned releases works properly."""
    # Create our user, and setup their collection
    user, token = create_user_token(session)
    master_set = session.query(Release).filter(
        Release.stub == "master-set").first()
    user_release = UserRelease(user_id=user.id, release_id=master_set.id)
    session.add(user_release)
    session.commit()

    # Verify that the filter works
    response = client.get(
        "/v2/cards",
        params={"releases": "mine"},
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == 200, response.json()
    assert len(response.json()["results"]) == 7, response.json()
コード例 #10
0
def create_card(data: CardIn,
                session: db.Session = Depends(get_session),
                _=Depends(admin_required)):
    """Admin-only. Adds a new card to the database.

    Releases will be implicitly created (and marked as private), if they do not already exist.
    There is no need to create the release ahead, as a result, which means you can populate a
    release entirely with this endpoint, and then just publish it when ready.

    Note that you must create conjurations *before* the cards that reference them.
    """
    # Implicitly create the release, if necessary
    release_stub = stubify(data.release)
    if not (release := (session.query(Release).filter(
            Release.stub == release_stub,
            Release.is_legacy.is_(False)).one_or_none())):
        release = Release(name=data.release, stub=release_stub)
        session.add(release)
        session.commit()
コード例 #11
0
def test_get_releases_legacy(client: TestClient, session: db.Session):
    """Releases list must only show legacy releases when they are requested"""
    master_set = Release(name="Master Set")
    master_set.is_public = True
    session.add(master_set)
    core_set = Release(name="Core Set")
    core_set.is_legacy = True
    core_set.is_public = True
    session.add(core_set)
    session.commit()
    response = client.get("/v2/releases")
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert len(data) == 1
    assert data[0]["stub"] == master_set.stub
    response = client.get("/v2/releases", params={"show_legacy": True})
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert len(data) == 1
    assert data[0]["stub"] == core_set.stub
コード例 #12
0
def test_phg_release_filtration(client: TestClient, session: db.Session):
    """Must be able to filter for only PHG release when looking at legacy cards."""
    release = Release(name="PHG")
    release.is_public = True
    release.is_legacy = True
    release.is_phg = True
    session.add(release)
    fan_release = Release(name="Fan")
    fan_release.is_public = True
    fan_release.is_legacy = True
    session.add(fan_release)
    session.commit()
    phg_card = create_card(
        session,
        name="A",
        card_type="Action Spell",
        placement="Discard",
        release=release,
        text="Text.",
    )
    fan_card = create_card(
        session,
        name="B",
        card_type="Action Spell",
        placement="Discard",
        release=fan_release,
        text="Text.",
    )
    phg_card.is_legacy = True
    fan_card.is_legacy = True
    session.commit()

    response = client.get("/v2/cards",
                          params={
                              "show_legacy": True,
                              "releases": "phg"
                          })
    assert response.status_code == 200
    assert len(response.json()["results"]) == 1, response.json()
コード例 #13
0
def test_put_releases(client: TestClient, session: db.Session):
    """Putting my releases must work"""
    master_set = Release(name="Master Set")
    master_set.is_public = True
    session.add(master_set)
    first_expansion = Release(name="First Expansion")
    first_expansion.is_public = True
    session.add(first_expansion)
    session.commit()
    user, token = create_user_token(session)
    assert (session.query(UserRelease).filter(
        UserRelease.user_id == user.id).count() == 0)
    response = client.put(
        "/v2/releases/mine",
        json=[master_set.stub],
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert data[0]["stub"] == master_set.stub
    assert data[0]["is_mine"] == True
    assert data[1]["is_mine"] == False
コード例 #14
0
ファイル: stream.py プロジェクト: onecrayon/api.ashes.live
def update_subscription_for_user(
    session: db.Session,
    user: "******",
    source_entity_id: int,
    last_seen_entity_id: int = None,
):
    """Create or update the user's subscription to the given entity ID.

    **Please note:** this method does not commit the changes! You must flush the session in the
    invoking method.
    """
    subscription = (session.query(Subscription).filter(
        Subscription.user_id == user.id,
        Subscription.source_entity_id == source_entity_id,
    ).first())
    if not subscription:
        subscription = Subscription(
            user_id=user.id,
            source_entity_id=source_entity_id,
            last_seen_entity_id=last_seen_entity_id,
        )
    else:
        subscription.last_seen_entity_id = last_seen_entity_id
    session.add(subscription)
コード例 #15
0
def test_get_releases_mine(client: TestClient, session: db.Session):
    """Releases list must mark which releases are in the user's collection"""
    master_set = Release(name="Master Set")
    master_set.is_public = True
    session.add(master_set)
    first_expansion = Release(name="First Expansion")
    first_expansion.is_public = True
    session.add(first_expansion)
    session.commit()
    user, token = create_user_token(session)
    session.add(UserRelease(release_id=master_set.id, user_id=user.id))
    session.commit()
    response = client.get(
        "/v2/releases",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert data[0]["stub"] == master_set.stub
    assert data[0]["is_mine"] == True
    assert data[1]["is_mine"] == False
コード例 #16
0
ファイル: deck.py プロジェクト: onecrayon/api.ashes.live
def create_or_update_deck(
    session: db.Session,
    current_user: "******",
    phoenixborn: Card,
    deck_id: int = None,
    title: str = None,
    description: str = None,
    dice: List[Dict[str, Union[str, int]]] = None,
    cards: List[Dict[str, Union[str, int]]] = None,
    first_five: List[str] = None,
    effect_costs: List[str] = None,
    tutor_map: Dict[str, str] = None,
) -> "Deck":
    """Creates or updates a deck in place."""
    now = datetime.utcnow()
    if deck_id:
        deck = (session.query(Deck).options(
            db.joinedload("cards"),
            db.joinedload("dice"),
            db.joinedload("selected_cards"),
        ).get(deck_id))
        deck.title = title
        deck.description = description
        deck.phoenixborn_id = phoenixborn.id
        deck.modified = now
    else:
        deck = Deck(
            entity_id=create_entity(session),
            title=title,
            description=description,
            user_id=current_user.id,
            phoenixborn_id=phoenixborn.id,
            is_snapshot=False,
            is_public=False,
        )

    # Update the dice listing
    deck_dice: List[DeckDie] = []
    total_dice = 0
    if dice:
        for die_dict in dice:
            die = die_dict.get("name")
            count = die_dict.get("count")
            if count:
                if total_dice + count > 10:
                    count = 10 - total_dice
                if count == 0:
                    break
                total_dice = total_dice + count
                deck_dice.append(
                    DeckDie(die_flag=DiceFlags[die].value, count=count))
    deck.dice = deck_dice

    # And then the card listing
    deck_cards: List[DeckCard] = []
    card_stub_counts = {x["stub"]: x["count"] for x in (cards or [])}
    card_stubs = set(card_stub_counts.keys())
    if first_five:
        card_stubs.update(first_five)
    if effect_costs:
        card_stubs.update(effect_costs)
    if tutor_map:
        card_stubs.update(tutor_map.keys())
        card_stubs.update(tutor_map.values())
    minimal_cards = (session.query(Card.id, Card.stub, Card.name,
                                   Card.card_type,
                                   Card.phoenixborn).join(Card.release).filter(
                                       Card.stub.in_(card_stubs),
                                       Card.is_legacy.is_(False),
                                       Release.is_public == True,
                                   ).all())
    for card in minimal_cards:
        # Minimal cards could include bogus cards thanks to first_five list and similar, so fall
        #  back to zero to ensure this is something with a count
        count = card_stub_counts.get(card.stub, 0)
        # Make sure our count can't exceed 3
        count = count if count <= 3 else 3
        # Skip it if it's not part of the deck
        if count <= 0:
            continue
        if card.card_type == CardType.phoenixborn.value:
            raise PhoenixbornInDeck()
        if card.phoenixborn and card.phoenixborn != phoenixborn.name:
            raise BadPhoenixbornUnique(card)
        if card.card_type in (
                CardType.conjuration.value,
                CardType.conjured_alteration_spell.value,
        ):
            raise ConjurationInDeck(card)
        deck_cards.append(DeckCard(card_id=card.id, count=count))
    deck.cards = deck_cards

    # Save everything up!
    deck.selected_cards = []
    session.add(deck)
    session.commit()

    # And finally set selected cards (first five, paid effects, and tutored cards; used for stats)
    #  This happens after clearing them out above because SQLAlchemy cannot handle the three way
    #  composite index (tries to insert duplicates instead of updating intelligently based on
    #  tutor_card_id)
    stub_to_card = {x.stub: x for x in minimal_cards}
    selected_cards: List[DeckSelectedCard] = []
    if effect_costs:
        for card_stub in effect_costs:
            card = stub_to_card.get(card_stub)
            # TODO: remove pragmas once I've updated to Python 3.10 and this coverage bug is fixed
            if not card:  # pragma: no cover
                continue
            if first_five and card_stub not in first_five:
                selected_cards.append(
                    DeckSelectedCard(card_id=card.id, is_paid_effect=True))
    if first_five:
        for card_stub in first_five:
            card = stub_to_card.get(card_stub)
            if not card:  # pragma: no cover
                continue
            selected_cards.append(
                DeckSelectedCard(
                    card_id=card.id,
                    is_first_five=True,
                    is_paid_effect=card.stub in effect_costs
                    if effect_costs else False,
                ))
    if tutor_map:
        for tutor_stub, card_stub in tutor_map.items():
            tutor_card = stub_to_card.get(tutor_stub)
            card = stub_to_card.get(card_stub)
            if not tutor_card or not card:  # pragma: no cover
                continue
            selected_cards.append(
                DeckSelectedCard(card_id=card.id, tutor_card_id=tutor_card.id))
    deck.selected_cards = selected_cards
    session.commit()

    return deck
コード例 #17
0
ファイル: deck.py プロジェクト: onecrayon/api.ashes.live
def create_snapshot_for_deck(
    session: db.Session,
    user: "******",
    deck: "Deck",
    title: str = None,
    description: str = None,
    is_public=False,
    preconstructed_release_id: int = None,
    include_first_five=False,
) -> "Deck":
    """Creates a snapshot for the given deck"""
    entity_id = create_entity(session)
    snapshot = Deck(
        entity_id=entity_id,
        title=title,
        description=description,
        # In the interim while we are saving the cards and dice and so forth, we mark this as private so that it doesn't
        #  show up in any listings and break stuff in weird ways
        is_public=False,
        is_snapshot=True,
        is_preconstructed=bool(preconstructed_release_id),
        preconstructed_release=preconstructed_release_id,
        source_id=deck.id,
        user_id=user.id,
        phoenixborn_id=deck.phoenixborn_id,
    )
    # Save our snapshot so that we have an ID
    session.add(snapshot)
    session.commit()
    # Now duplicate the cards, dice, and selected cards for the given deck
    if deck.cards:
        for deck_card in deck.cards:
            session.add(
                DeckCard(
                    deck_id=snapshot.id,
                    card_id=deck_card.card_id,
                    count=deck_card.count,
                ))
    if deck.dice:
        for deck_die in deck.dice:
            session.add(
                DeckDie(
                    deck_id=snapshot.id,
                    die_flag=deck_die.die_flag,
                    count=deck_die.count,
                ))
    if deck.selected_cards and (include_first_five or not is_public):
        for deck_selected_card in deck.selected_cards:
            session.add(
                DeckSelectedCard(
                    deck_id=snapshot.id,
                    card_id=deck_selected_card.card_id,
                    tutor_card_id=deck_selected_card.tutor_card_id,
                    is_first_five=deck_selected_card.is_first_five,
                    is_paid_effect=deck_selected_card.is_paid_effect,
                ))
    # Flip our public flag now that we've populated the deck details, if necessary
    if is_public:
        snapshot.is_public = True
        # We also need to publish to the Stream for public snapshots
        refresh_stream_for_entity(
            session,
            entity_id=snapshot.entity_id,
            entity_type="deck",
            source_entity_id=deck.entity_id,
        )
        # And finally we need to update the user's subscription to mark the last_seen_entity_id
        update_subscription_for_user(
            session,
            user=user,
            source_entity_id=deck.entity_id,
            last_seen_entity_id=snapshot.entity_id,
        )
    session.commit()
    return snapshot
コード例 #18
0
def create_cards_for_decks(session: db.Session):
    """This utility function populates a set of cards appropriate for testing deck building"""
    # First create our two releases
    master_set = Release("Master Set")
    master_set.is_public = True
    expansion = Release("Expansion")
    expansion.is_public = True
    session.add(master_set)
    session.add(expansion)
    session.commit()
    # Then create two Phoenixborns (with uniques), and 18 other cards spanning all card types
    #  (to ensure that we can filter by release, preconstructed decks, etc.)
    card_dicts = [
        {
            "name": "One Phoenixborn",
            "card_type": "Phoenixborn",
            "release": master_set,
            "text": "Command Strike: [[side]] - 2 [[basic]]: Do stuff.",
            "effect_magic_cost": "2 [[basic]]",
            "battlefield": 4,
            "life": 20,
            "spellboard": 5,
        },
        {
            "name": "One Conjuration A",
            "card_type": "Conjuration",
            "release": master_set,
            "placement": "Battlefield",
            "text": "* Consume: Do stuff.",
            "life": 4,
            "attack": "X",
            "copies": 1,
            "recover": 3,
            "phoenixborn": "One Phoenixborn",
        },
        {
            "name": "Summon One Conjuration A",
            "card_type": "Ready Spell",
            "release": master_set,
            "placement": "Spellboard",
            "cost": ["[[main]]"],
            "text": "[[main]] - [[exhaust]] - 1 [[charm:power]] - 1 [[natural:power]]: Place a [[One Conjuration A]] conjuration onto your battlefield.",
            "phoenixborn": "One Phoenixborn",
        },
        {
            "name": "One Conjuration B",
            "card_type": "Conjuration",
            "release": master_set,
            "placement": "Battlefield",
            "text": "Unit Guard: Do stuff.",
            "attack": 0,
            "life": 2,
            "recover": 0,
            "copies": 2,
        },
        {
            "name": "Summon One Conjuration B",
            "card_type": "Ready Spell",
            "release": master_set,
            "placement": "Spellboard",
            "cost": ["[[main]]", "1 [[charm:class]]"],
            "text": "[[main]] - [[exhaust]] - 1 [[natural:class]]: Place a [[One Conjuration B]] conjuration onto your battlefield.",
        },
        {
            "name": "One Ready Spell A",
            "card_type": "Ready Spell",
            "release": master_set,
            "placement": "Spellboard",
            "cost": ["[[main]]"],
            "text": "[[side]] - [[exhaust]] - 2 [[charm:class]]: Do stuff.",
        },
        {
            "name": "One Ready Spell B",
            "card_type": "Ready Spell",
            "release": master_set,
            "placement": "Spellboard",
            "cost": ["[[side]]", "1 [[basic]]"],
            "text": "[[main]] - [[exhaust]] - 1 [[natural:class]] or 1 [[sympathy:class]]: Do stuff.",
        },
        {
            "name": "One Action Spell A",
            "card_type": "Action Spell",
            "release": master_set,
            "placement": "Discard",
            "cost": ["[[main]]", "2 [[natural:power]]"],
            "text": "Do stuff.",
        },
        {
            "name": "One Action Spell B",
            "card_type": "Action Spell",
            "release": master_set,
            "placement": "Discard",
            "cost": ["[[main]]", "1 [[charm:class]]", "1 [[basic]]"],
            "text": "Do stuff.",
        },
        {
            "name": "One Action Spell C",
            "card_type": "Action Spell",
            "release": master_set,
            "placement": "Discard",
            "cost": ["[[main]]", "1 [[charm:class]]", "1 [[basic]]"],
            "text": "Do stuff.",
        },
        {
            "name": "One Reaction Spell",
            "card_type": "Reaction Spell",
            "release": master_set,
            "placement": "Discard",
            "cost": ["1 [[charm:power]]"],
            "text": "You may play this spell after an opponent targets a unit you control with a spell, ability, or dice power. Do stuff.",
        },
        {
            "name": "One Alteration Spell",
            "card_type": "Alteration Spell",
            "release": master_set,
            "placement": "Unit",
            "cost": ["[[side]]", "1 [[natural:class]]"],
            "text": "This unit now has the following ability:\n\n* Armored 1: Do stuff.",
            "life": "+1",
        },
        {
            "name": "One Ally",
            "card_type": "Ally",
            "release": master_set,
            "placement": "Battlefield",
            "cost": ["[[main]]", "1 [[charm:class]]"],
            "text": "Song of Sorrow: [[side]] - [[exhaust]]: Do stuff.",
            "attack": 1,
            "life": 1,
            "recover": 1,
        },
        # And define cards for a single expansion
        {
            "name": "Two Conjured Alteration Spell",
            "card_type": "Conjured Alteration Spell",
            "release": expansion,
            "placement": "Unit",
            "life": "+1",
            "copies": 5,
            "phoenixborn": "Two Phoenixborn",
        },
        {
            "name": "Two Phoenixborn",
            "text": "Ice Buff: [[side]] - [[exhaust]]: Attach an [[Two Conjured Alteration Spell]] conjured alteration spell to a target unit you control.",
            "card_type": "Phoenixborn",
            "release": expansion,
            "battlefield": 6,
            "life": 17,
            "spellboard": 4,
        },
        {
            "name": "Two's Reaction Spell",
            "card_type": "Reaction Spell",
            "release": expansion,
            "placement": "Discard",
            "cost": ["2 [[basic]]"],
            "text": "You may play this spell after a unit you control is dealt damage by a unit's attack. Prevent that damage from being received. Destroy that target attacking unit.",
            "phoenixborn": "Two Phoenixborn",
        },
        {
            "name": "Two Conjuration A",
            "card_type": "Conjuration",
            "release": expansion,
            "placement": "Battlefield",
            "text": "* Skin Morph 2: Do stuff.",
            "attack": 3,
            "life": 2,
            "recover": 0,
            "copies": 3,
        },
        {
            "name": "Summon Two Conjuration A",
            "card_type": "Ready Spell",
            "release": expansion,
            "placement": "Spellboard",
            "cost": ["[[main]]"],
            "text": "[[main]] - [[exhaust]] - 2 [[natural:class]] - 1 [[basic]]: Place an [[Two Conjuration A]] conjuration onto your battlefield.\n\nFocus 2: Do stuff.",
        },
        {
            "name": "Two Conjuration D",
            "card_type": "Conjuration",
            "release": expansion,
            "placement": "Battlefield",
            "text": "* Inheritance 1: Do stuff.",
            "attack": 3,
            "life": 2,
            "recover": 0,
            "copies": 6,
        },
        {
            "name": "Two Conjuration C",
            "card_type": "Conjuration",
            "release": expansion,
            "placement": "Battlefield",
            "text": "Blossom: [[main]]: Place up to 2 [[Two Conjuration D]] conjurations onto your battlefield.",
            "attack": 0,
            "life": 2,
            "recover": 0,
            "copies": 3,
        },
        {
            "name": "Two Conjuration B",
            "card_type": "Conjuration",
            "release": expansion,
            "placement": "Battlefield",
            "text": "* Germinate: When this unit is destroyed, place a [[Two Conjuration C]] conjuration onto your battlefield.",
            "attack": 2,
            "life": 1,
            "recover": 0,
            "copies": 3,
        },
        {
            "name": "Summon Two Conjuration B",
            "card_type": "Ready Spell",
            "release": expansion,
            "placement": "Spellboard",
            "cost": ["[[main]]"],
            "text": "[[main]] - [[exhaust]] - 1 [[natural:class]] - 1 [[sympathy:class]]: Place an [[Two Conjuration B]] conjuration onto your battlefield.\n\nFocus 1: Do stuff.",
        },
        {
            "name": "Two Ready Spell",
            "card_type": "Ready Spell",
            "release": expansion,
            "placement": "Spellboard",
            "cost": ["[[main]]"],
            "text": "[[main]] - [[exhaust]] - 1 [[natural:class]]: Do stuff.",
        },
        {
            "name": "Two Reaction Spell",
            "card_type": "Reaction Spell",
            "release": expansion,
            "placement": "Discard",
            "cost": ["1 [[natural:class]]"],
            "text": "You may play this spell after a unit with a life value of 2 or less comes into play. Destroy that target unit.",
        },
        {
            "name": "Two Alteration Spell",
            "card_type": "Alteration Spell",
            "release": expansion,
            "placement": "Unit",
            "cost": ["[[main]]", "1 [[natural:class]]"],
            "text": "* When attaching this spell, place 3 status tokens on this spell. Discard this spell when it no longer has any status tokens on it. As long as this spell is attached to this unit, this unit is considered to be exhausted. This unit now has the following ability:\n\n* Thaw: [[side]]: Remove 1 status token from a Deep Freeze alteration spell attached to this unit.",
        },
        {
            "name": "Two Action Spell",
            "card_type": "Action Spell",
            "release": expansion,
            "placement": "Discard",
            "cost": ["[[main]]", "2 [[natural:class]]"],
            "text": "Deal 2 damage to a target unit. Remove 2 status tokens from that unit.",
        },
        {
            "name": "Two Ally A",
            "card_type": "Ally",
            "release": expansion,
            "placement": "Battlefield",
            "cost": ["[[main]]", "2 [[natural:class]]"],
            "text": "* Armored 1: After this unit is dealt damage, prevent 1 damage from being received.",
            "attack": 3,
            "life": 1,
            "recover": 1,
        },
        {
            "name": "Two Ally B",
            "card_type": "Ally",
            "release": expansion,
            "placement": "Battlefield",
            "cost": ["[[main]]", "1 [[sympathy:class]]", "1 [[basic]]"],
            "text": "* Last Orders 1: When this unit is destroyed, you may spend 1 [[basic]] to do stuff.\n\n* Inheritance 1: Do stuff.",
            "attack": 2,
            "life": 2,
            "recover": 1,
        },
        {
            "name": "Two Ally C",
            "card_type": "Ally",
            "release": expansion,
            "placement": "Battlefield",
            "cost": ["[[main]]", "2 [[natural:class]]"],
            "text": "Slumbering 1: Do stuff.",
            "attack": 4,
            "life": 4,
            "recover": 2,
        },
    ]
    # Create our cards
    for card_dict in card_dicts:
        create_card(session, **card_dict)
コード例 #19
0
ファイル: stream.py プロジェクト: onecrayon/api.ashes.live
def create_entity(session: db.Session) -> int:
    """Creates and returns a new entity ID"""
    entity = Streamable()
    session.add(entity)
    session.commit()
    return entity.entity_id
コード例 #20
0
def clone_deck(
    deck_id: int,
    direct_share_uuid: UUID4 = Query(
        None,
        description="Optional direct share UUID, if cloning a privately shared deck.",
    ),
    session: db.Session = Depends(get_session),
    current_user: "******" = Depends(login_required),
):
    """Clone a snapshot, deck, or private share.

    Allows users to create a new deck that is an exact copy of:

    * one of their own decks
    * a public snapshot of someone else's deck
    * a privately shared deck or snapshot

    Returns a copy of the deck suitable for editing.

    Note that unlike the legacy site, cloning a deck in v2 will automatically create an initial snapshot of the source
    deck. This allows viewing the source deck even if it is deleted, overwritten, or otherwise inaccessible.
    """
    # Simple check if the target exists first (no need for joins)
    snapshot_or_deck_filters = [
        db.and_(
            Deck.is_public.is_(True),
            Deck.is_snapshot.is_(True),
        ),
        Deck.user_id == current_user.id,
    ]
    if direct_share_uuid:
        snapshot_or_deck_filters.append(
            Deck.direct_share_uuid == direct_share_uuid,
        )
    valid_deck_filters = (
        db.or_(*snapshot_or_deck_filters),
        Deck.id == deck_id,
        Deck.is_legacy.is_(False),
        Deck.is_deleted.is_(False),
    )
    deck = session.query(Deck.id).filter(*valid_deck_filters).first()
    if not deck:
        raise NotFoundException(detail="Invalid ID for cloning.")
    # Then we grab a new entity_id first because it causes a commit and kills the process otherwise
    entity_id = create_entity(session)
    # Then we can finally grab our full deck and copy it
    deck = (
        session.query(Deck)
        .options(
            db.joinedload("cards"),
            db.joinedload("dice"),
            db.joinedload("selected_cards"),
        )
        .filter(*valid_deck_filters)
        .first()
    )
    # Create a clone of our deck object (transient cloning was too error-prone, so we're doing everything by hand)
    cloned_deck = Deck(
        entity_id=entity_id,
        title=f"Copy of {deck.title}",
        description=deck.description,
        # Only track source IDs if we own the source or it's a public snapshot
        source_id=deck.id
        if current_user.id == deck.user_id or (deck.is_snapshot and deck.is_public)
        else None,
        user_id=current_user.id,
        phoenixborn_id=deck.phoenixborn_id,
    )
    session.add(cloned_deck)
    session.commit()
    for die in deck.dice:
        session.add(
            DeckDie(deck_id=cloned_deck.id, die_flag=die.die_flag, count=die.count)
        )
    for card in deck.cards:
        session.add(
            DeckCard(deck_id=cloned_deck.id, card_id=card.card_id, count=card.count)
        )
    for card in deck.selected_cards:
        session.add(
            DeckSelectedCard(
                deck_id=cloned_deck.id,
                card_id=card.card_id,
                tutor_card_id=card.tutor_card_id,
                is_first_five=card.is_first_five,
                is_paid_effect=card.is_paid_effect,
            )
        )
    session.commit()
    # Finally create an initial snapshot for the deck so we can see its original state even if the source changes
    create_snapshot_for_deck(
        session,
        current_user,
        cloned_deck,
        title=f"Source: {deck.title}",
        is_public=False,
        include_first_five=True,
    )
    return deck_to_dict(session, deck=cloned_deck)
コード例 #21
0
ファイル: conftest.py プロジェクト: onecrayon/api.ashes.live
def _create_cards_for_filtration(session: db.Session, is_legacy=False):
    """Populates database with a minimum viable list of one of each card type"""
    # First create our two releases
    master_set = models.Release("Master Set")
    master_set.is_legacy = is_legacy
    master_set.is_public = True
    expansion = models.Release("First Expansion")
    expansion.is_legacy = is_legacy
    expansion.is_public = True
    session.add(master_set)
    session.add(expansion)
    session.commit()
    # Then create one of every type of card, with a mixture of all the different things that can be
    #  included to ensure that the automatic dice sorting and so forth works properly
    card_dicts = [
        {
            "name": "Example Conjuration",
            "card_type": "Conjuration",
            "placement": "Battlefield",
            "release": master_set,
            "attack": 0,
            "life": 2,
            "recover": 0,
            "copies": 3,
        },
        {
            "name": "Example Conjured Alteration",
            "card_type": "Conjured Alteration Spell",
            "placement": "Unit",
            "phoenixborn": "Example Phoenixborn",
            "release": master_set,
            "text": "Whoops: 1 [[basic]] - 1 [[discard]]: Discard this spell.",
            "effect_magic_cost": "1 [[basic]]",
            "attack": "-2",
            "copies": 2,
        },
        {
            "name": "Example Phoenixborn",
            "card_type": "Phoenixborn",
            "release": master_set,
            "text":
            "Mess With Them: [[main]] - 1 [[illusion:class]]: Place a [[Example Conjured Alteration]] conjured alteration spell on opponent's unit.",
            "effect_magic_cost": "1 [[illusion:class]]",
            "battlefield": 5,
            "life": 16,
            "spellboard": 4,
            "can_effect_repeat": True,
        },
        {
            "name": "Summon Example Conjuration",
            "card_type": "Ready Spell",
            "placement": "Spellboard",
            "release": master_set,
            "cost": "[[main]] - 1 [[basic]] - [[side]] / 1 [[discard]]",
            "text":
            "1 [[charm:class]]: Place a [[Example Conjuration]] conjuration on your battlefield.",
            "effect_magic_cost": ["1 [[charm:class]]"],
        },
        {
            "name": "Example Ally Conjuration",
            "card_type": "Conjuration",
            "placement": "Battlefield",
            "phoenixborn": "Example Phoenixborn",
            "release": master_set,
            "attack": 2,
            "life": 1,
            "recover": 0,
            "copies": 2,
        },
        {
            "name": "Example Ally",
            "card_type": "Ally",
            "placement": "Battlefield",
            "phoenixborn": "Example Phoenixborn",
            "release": master_set,
            "cost":
            ["[[main]]", ["1 [[natural:power", "1 [[illusion:power]]"]],
            "text":
            "Stuffiness: [[main]] - [[exhaust]] - 1 [[natural:class]] / 1 [[illusion:class]]: Place a [[Example Ally Conjuration]] conjuration on your battlefield.",
            "effect_magic_cost": "1 [[natural:class]] / 1 [[illusion:class]]",
            "attack": 2,
            "life": 1,
            "recover": 1,
        },
        {
            "name": "Example Alteration",
            "card_type": "Alteration Spell",
            "placement": "Unit",
            "release": master_set,
            "cost": "[[side]] - 1 [[basic]] - 1 [[discard]]",
            "attack": "+2",
            "recover": "-1",
        },
        {
            "name": "Example Ready Spell",
            "card_type": "Ready Spell",
            "placement": "Spellboard",
            "release": expansion,
            "cost": "[[side]]",
            "text": "[[main]] - [[exhaust]]: Do more things.",
        },
        {
            "name": "Example Action",
            "card_type": "Action Spell",
            "placement": "Discard",
            "release": expansion,
            "cost": "[[main]] - 1 [[time:power]] - 1 [[basic]]",
            "text":
            "If you spent a [[sympathy:power]] to pay for this card, do more stuff.",
            "alt_dice": ["sympathy"],
        },
        {
            "name": "Example Reaction",
            "card_type": "Reaction Spell",
            "placement": "Discard",
            "release": expansion,
            "cost": "1 [[divine:class]] / 1 [[ceremonial:class]]",
            "text": "Do a happy dance.",
        },
    ]
    cards = []
    # Create our cards
    for card_dict in card_dicts:
        cards.append(create_card(session, **card_dict))
    if is_legacy:
        for card in cards:
            card.is_legacy = True
        session.commit()
コード例 #22
0
ファイル: card.py プロジェクト: onecrayon/api.ashes.live
def create_card(
    session: db.Session,
    name: str,
    card_type: str,
    release: "Release",
    placement: str = None,
    text: str = None,
    cost: Union[List[str], str, None] = None,
    effect_magic_cost: Union[List[str], str, None] = None,
    can_effect_repeat: bool = False,
    dice: List[str] = None,
    alt_dice: List[str] = None,
    phoenixborn: str = None,
    attack: str = None,
    battlefield: str = None,
    life: str = None,
    recover: str = None,
    spellboard: str = None,
    copies: int = None,
) -> "Card":
    """Creates a card, generating the necessary JSON and cost info"""
    card = Card()
    card.name = name
    card.stub = stubify(name)
    card.phoenixborn = phoenixborn
    card.card_type = card_type
    card.placement = placement
    card.release_id = release.id
    card.search_text = f"{card.name}\n"
    if card.phoenixborn:
        card.search_text += f"{card.phoenixborn}\n"
    card.is_summon_spell = name.startswith("Summon ")
    existing_conjurations = None
    if text:
        # Remove apostrophes and formatting characters from search text to ensure words are treated as lexemes
        card.search_text += re.sub(
            r"\n+", " ",
            text.replace("[[", "").replace("]]", "").replace("'", ""))
        # Check for conjurations before we do any more work
        conjuration_stubs = set()
        for match in re.finditer(
                r"\[\[([A-Z][A-Za-z' ]+)\]\](?=[ ](?:(?:conjuration|conjured alteration spell)s?|or))",
                text,
        ):
            conjuration_stubs.add(stubify(match.group(1)))
        existing_conjurations = (session.query(
            Card.id, Card.stub,
            Card.name).filter(Card.stub.in_(conjuration_stubs),
                              Card.is_legacy.is_(False)).all())
        existing_stubs = set(x.stub for x in existing_conjurations)
        missing_conjurations = conjuration_stubs.symmetric_difference(
            existing_stubs)
        if missing_conjurations:
            raise MissingConjurations(
                f"The following conjurations must be added first: {', '.join([x for x in missing_conjurations])}"
            )

    if copies is not None:
        card.copies = copies
    card.entity_id = create_entity(session)
    cost_list = re.split(r"\s+-\s+", cost) if isinstance(cost, str) else cost
    weight = 0
    json_cost_list = []
    if cost_list:
        for cost_entry in cost_list:
            split_cost = (re.split(r"\s+(?:/|or)\s+", cost_entry)
                          if isinstance(cost_entry, str) else cost_entry)
            if len(split_cost) > 1:
                first_weight = parse_cost_to_weight(split_cost[0])
                second_weight = parse_cost_to_weight(split_cost[1])
                weight += max(first_weight, second_weight)
                json_cost_list.append(split_cost)
            else:
                weight += parse_cost_to_weight(split_cost[0])
                json_cost_list.append(split_cost[0])
    card.cost_weight = weight
    # Extract our effect costs into a list of strings and lists
    effect_cost_list = []
    effect_costs = (re.split(r"\s+-\s+", effect_magic_cost) if isinstance(
        effect_magic_cost, str) else effect_magic_cost)
    if effect_costs:
        for cost_entry in effect_costs:
            split_cost = (re.split(r"\s+(?:/|or)\s+", cost_entry)
                          if isinstance(cost_entry, str) else cost_entry)
            effect_cost_list.append(split_cost) if len(
                split_cost) > 1 else effect_cost_list.append(split_cost[0])
    # Convert our cost lists into magicCost and effectMagicCost mappings
    json_magic_cost = parse_costs_to_mapping(json_cost_list)
    json_effect_cost = parse_costs_to_mapping(effect_cost_list)
    # And finally, convert our mappings into lists of required dice
    dice_set = set()
    alt_dice_set = set()
    for dice_type in list(json_magic_cost.keys()) + list(
            json_effect_cost.keys()):
        both_types = dice_type.split(" / ")
        if len(both_types) > 1:
            alt_dice_set.add(dice_name_from_cost(both_types[0]))
            alt_dice_set.add(dice_name_from_cost(both_types[1]))
        else:
            dice_set.add(dice_name_from_cost(both_types[0]))
    if dice is None:
        dice = list(dice_set)
    if alt_dice is None:
        alt_dice = list(alt_dice_set)
    card.dice_flags = Card.dice_to_flags(dice)
    card.alt_dice_flags = Card.dice_to_flags(alt_dice)
    json_data = {
        "name": card.name,
        "stub": card.stub,
        "type": card.card_type,
        "release": {
            "name": release.name,
            "stub": release.stub,
        },
    }
    if existing_conjurations:
        json_data["conjurations"] = [{
            "name": x.name,
            "stub": x.stub
        } for x in existing_conjurations]
    if placement:
        json_data["placement"] = placement
    if json_cost_list:
        json_data["cost"] = json_cost_list
    if dice:
        json_data["dice"] = dice
    if alt_dice:
        json_data["altDice"] = alt_dice
    if json_magic_cost:
        json_data["magicCost"] = json_magic_cost
    if json_effect_cost:
        json_data["effectMagicCost"] = json_effect_cost
    if text:
        json_data["text"] = text
    if phoenixborn is not None:
        json_data["phoenixborn"] = phoenixborn
    if attack is not None:
        json_data["attack"] = str_or_int(attack)
    if battlefield is not None:
        json_data["battlefield"] = str_or_int(battlefield)
    if life is not None:
        json_data["life"] = str_or_int(life)
    if recover is not None:
        json_data["recover"] = str_or_int(recover)
    if spellboard is not None:
        json_data["spellboard"] = str_or_int(spellboard)
    if copies is not None:
        json_data["copies"] = copies
    if can_effect_repeat:
        json_data["effectRepeats"] = True
    card.json = json_data
    session.add(card)
    session.commit()
    # Now that we have a card entry, we can populate the conjuration relationship(s)
    if existing_conjurations:
        for conjuration in existing_conjurations:
            session.add(
                CardConjuration(card_id=card.id,
                                conjuration_id=conjuration.id))
        session.commit()
    return card